blob: 096366cf88fb81bcce743ac046e02b95d34b057c [file] [log] [blame]
/*
* 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.icu.util.Calendar;
import android.os.Build;
import androidx.annotation.DoNotInline;
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.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
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 mSKDevice = null;
private SecretKey mSKReader = null;
private int mSKDeviceCounter;
private int mSKReaderCounter;
private android.security.identity.IdentityCredential mCredential = null;
HardwareIdentityCredential(android.security.identity.IdentityCredential credential) {
mCredential = credential;
}
@SuppressWarnings("deprecation")
@Override
public @NonNull KeyPair createEphemeralKeyPair() {
if (mEphemeralKeyPair == null) {
mEphemeralKeyPair = mCredential.createEphemeralKeyPair();
}
return mEphemeralKeyPair;
}
@SuppressWarnings("deprecation")
@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 (mSKDevice != 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.cborEncode(Util.cborBuildTaggedByteString(mSessionTranscript));
byte[] salt = MessageDigest.getInstance("SHA-256").digest(sessionTranscriptBytes);
byte[] info = new byte[] {'S', 'K', 'D', 'e', 'v', 'i', 'c', 'e'};
byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
mSKDevice = new SecretKeySpec(derivedKey, "AES");
info = new byte[] {'S', 'K', 'R', 'e', 'a', 'd', 'e', 'r'};
derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
mSKReader = new SecretKeySpec(derivedKey, "AES");
mSKDeviceCounter = 1;
mSKReaderCounter = 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, mSKDeviceCounter);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec encryptionParameterSpec = new GCMParameterSpec(128, iv.array());
cipher.init(Cipher.ENCRYPT_MODE, mSKDevice, encryptionParameterSpec);
messageCiphertextAndAuthTag = cipher.doFinal(messagePlaintext);
} catch (BadPaddingException
| IllegalBlockSizeException
| NoSuchPaddingException
| InvalidKeyException
| NoSuchAlgorithmException
| InvalidAlgorithmParameterException e) {
throw new RuntimeException("Error encrypting message", e);
}
mSKDeviceCounter += 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, mSKReaderCounter);
byte[] plainText = null;
try {
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, mSKReader, new GCMParameterSpec(128,
iv.array()));
plainText = cipher.doFinal(messageCiphertext);
} catch (BadPaddingException
| IllegalBlockSizeException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchAlgorithmException
| NoSuchPaddingException e) {
throw new MessageDecryptionException("Error decrypting message", e);
}
mSKReaderCounter += 1;
return plainText;
}
@Override
public @NonNull
Collection<X509Certificate> getCredentialKeyCertificateChain() {
return mCredential.getCredentialKeyCertificateChain();
}
@SuppressWarnings("deprecation")
@Override
public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) {
mCredential.setAllowUsingExhaustedKeys(allowUsingExhaustedKeys);
}
@Override
@Nullable
public BiometricPrompt.CryptoObject getCryptoObject() {
BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(mCredential);
return cryptoObject;
}
@SuppressWarnings("deprecation")
@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();
}
@SuppressWarnings("deprecation")
@Override
public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) {
mCredential.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey);
}
@Override
public @NonNull
Collection<X509Certificate> getAuthKeysNeedingCertification() {
return mCredential.getAuthKeysNeedingCertification();
}
@SuppressWarnings("deprecation")
@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);
}
}
@SuppressWarnings("deprecation")
@Override
public @NonNull
int[] getAuthenticationDataUsageCount() {
return mCredential.getAuthenticationDataUsageCount();
}
@RequiresApi(Build.VERSION_CODES.S)
private static class ApiImplS {
@SuppressWarnings("deprecation")
@DoNotInline
static void callSetAllowUsingExpiredKeys(
@NonNull android.security.identity.IdentityCredential credential,
boolean allowUsingExpiredKeys) {
credential.setAllowUsingExpiredKeys(allowUsingExpiredKeys);
}
@DoNotInline
static void callStoreStaticAuthenticationData(
@NonNull android.security.identity.IdentityCredential credential,
@NonNull X509Certificate authenticationKey,
@NonNull Instant expirationDate,
@NonNull byte[] staticAuthData)
throws android.security.identity.UnknownAuthenticationKeyException {
credential.storeStaticAuthenticationData(authenticationKey,
expirationDate,
staticAuthData);
}
@DoNotInline
static @NonNull byte[] callProveOwnership(
@NonNull android.security.identity.IdentityCredential credential,
@NonNull byte[] challenge) {
return credential.proveOwnership(challenge);
}
@DoNotInline
static @NonNull byte[] callDelete(
@NonNull android.security.identity.IdentityCredential credential,
@NonNull byte[] challenge) {
return credential.delete(challenge);
}
@DoNotInline
static @NonNull byte[] callUpdate(
@NonNull android.security.identity.IdentityCredential credential,
@NonNull android.security.identity.PersonalizationData personalizationData) {
return credential.update(personalizationData);
}
}
@Override
public void setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ApiImplS.callSetAllowUsingExpiredKeys(mCredential, allowUsingExpiredKeys);
} else {
throw new UnsupportedOperationException();
}
}
@Override
public void storeStaticAuthenticationData(
@NonNull X509Certificate authenticationKey,
@NonNull Calendar expirationDate,
@NonNull byte[] staticAuthData)
throws UnknownAuthenticationKeyException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
Instant expirationDateAsInstant =
Instant.ofEpochMilli(expirationDate.getTimeInMillis());
ApiImplS.callStoreStaticAuthenticationData(mCredential,
authenticationKey,
expirationDateAsInstant,
staticAuthData);
} catch (android.security.identity.UnknownAuthenticationKeyException e) {
throw new UnknownAuthenticationKeyException(e.getMessage(), e);
}
} else {
throw new UnsupportedOperationException();
}
}
@Override
public @NonNull byte[] proveOwnership(@NonNull byte[] challenge) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ApiImplS.callProveOwnership(mCredential, challenge);
} else {
throw new UnsupportedOperationException();
}
}
@Override
public @NonNull byte[] delete(@NonNull byte[] challenge) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ApiImplS.callDelete(mCredential, challenge);
} else {
throw new UnsupportedOperationException();
}
}
@Override
public @NonNull byte[] update(@NonNull PersonalizationData personalizationData) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ApiImplS.callUpdate(mCredential,
HardwareWritableIdentityCredential.convertPDFromJetpack(personalizationData));
} else {
throw new UnsupportedOperationException();
}
}
}