HardwareIdentityCredential.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.os.Build;

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

import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collection;

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;

@RequiresApi(Build.VERSION_CODES.R)
class HardwareIdentityCredential extends IdentityCredential {

    private static final String TAG = "HardwareIdentityCredential";

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

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

    private int mEphemeralCounter;
    private int mReadersExpectedEphemeralCounter;

    private android.security.identity.IdentityCredential mCredential  = null;

    HardwareIdentityCredential(android.security.identity.IdentityCredential credential) {
        mCredential = credential;
    }

    @Override
    public @NonNull KeyPair createEphemeralKeyPair() {
        if (mEphemeralKeyPair == null) {
            mEphemeralKeyPair = mCredential.createEphemeralKeyPair();
        }
        return mEphemeralKeyPair;
    }

    @Override
    public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey)
            throws InvalidKeyException {
        mReaderEphemeralPublicKey = readerEphemeralPublicKey;
        mCredential.setReaderEphemeralPublicKey(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 mCredential.getCredentialKeyCertificateChain();
    }

    @Override
    public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) {
        mCredential.setAllowUsingExhaustedKeys(allowUsingExhaustedKeys);
    }

    @Override
    @Nullable
    public BiometricPrompt.CryptoObject getCryptoObject() {
        BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(mCredential);
        return cryptoObject;
    }

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

        android.security.identity.ResultData rd;
        try {
            rd = mCredential.getEntries(requestMessage,
                    entriesToRequest,
                    mSessionTranscript,
                    readerSignature);
        } catch (android.security.identity.NoAuthenticationKeyAvailableException e) {
            throw new NoAuthenticationKeyAvailableException(e.getMessage(), e);
        } catch (android.security.identity.InvalidReaderSignatureException e) {
            throw new InvalidReaderSignatureException(e.getMessage(), e);
        } catch (android.security.identity.InvalidRequestMessageException e) {
            throw new InvalidRequestMessageException(e.getMessage(), e);
        } catch (android.security.identity.EphemeralPublicKeyNotFoundException e) {
            throw new EphemeralPublicKeyNotFoundException(e.getMessage(), e);
        } catch (android.security.identity.SessionTranscriptMismatchException e) {
            throw new RuntimeException("Unexpected SessionMismatchException", e);
        }

        SimpleResultData.Builder builder = new SimpleResultData.Builder();
        builder.setMessageAuthenticationCode(rd.getMessageAuthenticationCode());
        builder.setAuthenticatedData(rd.getAuthenticatedData());
        builder.setStaticAuthenticationData(rd.getStaticAuthenticationData());

        for (String namespaceName : rd.getNamespaces()) {
            for (String entryName : rd.getEntryNames(namespaceName)) {
                int status = rd.getStatus(namespaceName, entryName);
                if (status == ResultData.STATUS_OK) {
                    byte[] value = rd.getEntry(namespaceName, entryName);
                    builder.addEntry(namespaceName, entryName, value);
                } else {
                    builder.addErrorStatus(namespaceName, entryName, status);
                }
            }
        }

        return builder.build();
    }

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

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

    @Override
    public void storeStaticAuthenticationData(@NonNull X509Certificate authenticationKey,
            @NonNull byte[] staticAuthData) throws UnknownAuthenticationKeyException {
        try {
            mCredential.storeStaticAuthenticationData(authenticationKey, staticAuthData);
        } catch (android.security.identity.UnknownAuthenticationKeyException e) {
            throw new UnknownAuthenticationKeyException(e.getMessage(), e);
        }
    }

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

}