| /* |
| * 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 android.security.identity; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.KeyPair; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import java.security.PublicKey; |
| import java.security.UnrecoverableKeyException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| |
| 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; |
| |
| class CredstoreIdentityCredential extends IdentityCredential { |
| |
| private static final String TAG = "CredstoreIdentityCredential"; |
| private String mCredentialName; |
| private @IdentityCredentialStore.Ciphersuite int mCipherSuite; |
| private Context mContext; |
| private ICredential mBinder; |
| private CredstorePresentationSession mSession; |
| private int mFeatureVersion; |
| |
| CredstoreIdentityCredential(Context context, String credentialName, |
| @IdentityCredentialStore.Ciphersuite int cipherSuite, |
| ICredential binder, |
| @Nullable CredstorePresentationSession session, |
| int featureVersion) { |
| mContext = context; |
| mCredentialName = credentialName; |
| mCipherSuite = cipherSuite; |
| mBinder = binder; |
| mSession = session; |
| mFeatureVersion = featureVersion; |
| } |
| |
| private KeyPair mEphemeralKeyPair = null; |
| private SecretKey mSecretKey = null; |
| private SecretKey mReaderSecretKey = null; |
| private int mEphemeralCounter; |
| private int mReadersExpectedEphemeralCounter; |
| |
| private void ensureEphemeralKeyPair() { |
| if (mEphemeralKeyPair != null) { |
| return; |
| } |
| try { |
| // This PKCS#12 blob is generated in credstore, using BoringSSL. |
| // |
| // The main reason for this convoluted approach and not just sending the decomposed |
| // key-pair is that this would require directly using (device-side) BouncyCastle which |
| // is tricky due to various API hiding efforts. So instead we have credstore generate |
| // this PKCS#12 blob. The blob is encrypted with no password (sadly, also, BoringSSL |
| // doesn't support not using encryption when building a PKCS#12 blob). |
| // |
| byte[] pkcs12 = mBinder.createEphemeralKeyPair(); |
| String alias = "ephemeralKey"; |
| char[] password = {}; |
| |
| KeyStore ks = KeyStore.getInstance("PKCS12"); |
| ByteArrayInputStream bais = new ByteArrayInputStream(pkcs12); |
| ks.load(bais, password); |
| PrivateKey privKey = (PrivateKey) ks.getKey(alias, password); |
| |
| Certificate cert = ks.getCertificate(alias); |
| PublicKey pubKey = cert.getPublicKey(); |
| |
| mEphemeralKeyPair = new KeyPair(pubKey, privKey); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } catch (KeyStoreException |
| | CertificateException |
| | UnrecoverableKeyException |
| | NoSuchAlgorithmException |
| | IOException e) { |
| throw new RuntimeException("Unexpected exception ", e); |
| } |
| } |
| |
| @Override |
| public @NonNull KeyPair createEphemeralKeyPair() { |
| ensureEphemeralKeyPair(); |
| return mEphemeralKeyPair; |
| } |
| |
| @Override |
| public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) |
| throws InvalidKeyException { |
| try { |
| byte[] uncompressedForm = |
| Util.publicKeyEncodeUncompressedForm(readerEphemeralPublicKey); |
| mBinder.setReaderEphemeralPublicKey(uncompressedForm); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| |
| ensureEphemeralKeyPair(); |
| |
| try { |
| KeyAgreement ka = KeyAgreement.getInstance("ECDH"); |
| ka.init(mEphemeralKeyPair.getPrivate()); |
| ka.doPhase(readerEphemeralPublicKey, true); |
| byte[] sharedSecret = ka.generateSecret(); |
| |
| byte[] salt = new byte[1]; |
| byte[] info = new byte[0]; |
| |
| salt[0] = 0x01; |
| byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32); |
| mSecretKey = new SecretKeySpec(derivedKey, "AES"); |
| |
| salt[0] = 0x00; |
| derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32); |
| mReaderSecretKey = new SecretKeySpec(derivedKey, "AES"); |
| |
| mEphemeralCounter = 1; |
| mReadersExpectedEphemeralCounter = 1; |
| |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException("Error performing key agreement", e); |
| } |
| } |
| |
| @Override |
| public @NonNull byte[] encryptMessageToReader(@NonNull byte[] messagePlaintext) { |
| 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 { |
| 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() { |
| try { |
| byte[] certsBlob = mBinder.getCredentialKeyCertificateChain(); |
| ByteArrayInputStream bais = new ByteArrayInputStream(certsBlob); |
| |
| Collection<? extends Certificate> certs = null; |
| try { |
| CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
| certs = factory.generateCertificates(bais); |
| } catch (CertificateException e) { |
| throw new RuntimeException("Error decoding certificates", e); |
| } |
| |
| ArrayList<X509Certificate> x509Certs = new ArrayList<>(); |
| for (Certificate cert : certs) { |
| x509Certs.add((X509Certificate) cert); |
| } |
| return x509Certs; |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| private boolean mAllowUsingExhaustedKeys = true; |
| private boolean mAllowUsingExpiredKeys = false; |
| private boolean mIncrementKeyUsageCount = true; |
| |
| @Override |
| public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) { |
| mAllowUsingExhaustedKeys = allowUsingExhaustedKeys; |
| } |
| |
| @Override |
| public void setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) { |
| mAllowUsingExpiredKeys = allowUsingExpiredKeys; |
| } |
| |
| @Override |
| public void setIncrementKeyUsageCount(boolean incrementKeyUsageCount) { |
| mIncrementKeyUsageCount = incrementKeyUsageCount; |
| } |
| |
| private boolean mOperationHandleSet = false; |
| private long mOperationHandle = 0; |
| |
| /** |
| * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an |
| * operation handle. |
| * |
| * @hide |
| */ |
| @Override |
| public long getCredstoreOperationHandle() { |
| if (!mOperationHandleSet) { |
| try { |
| mOperationHandle = mBinder.selectAuthKey(mAllowUsingExhaustedKeys, |
| mAllowUsingExpiredKeys, |
| mIncrementKeyUsageCount); |
| mOperationHandleSet = true; |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) { |
| // The NoAuthenticationKeyAvailableException will be thrown when |
| // the caller proceeds to call getEntries(). |
| } |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| return mOperationHandle; |
| } |
| |
| @NonNull |
| @Override |
| public ResultData getEntries( |
| @Nullable byte[] requestMessage, |
| @NonNull Map<String, Collection<String>> entriesToRequest, |
| @Nullable byte[] sessionTranscript, |
| @Nullable byte[] readerSignature) |
| throws SessionTranscriptMismatchException, NoAuthenticationKeyAvailableException, |
| InvalidReaderSignatureException, EphemeralPublicKeyNotFoundException, |
| InvalidRequestMessageException { |
| |
| RequestNamespaceParcel[] rnsParcels = new RequestNamespaceParcel[entriesToRequest.size()]; |
| int n = 0; |
| for (String namespaceName : entriesToRequest.keySet()) { |
| Collection<String> entryNames = entriesToRequest.get(namespaceName); |
| rnsParcels[n] = new RequestNamespaceParcel(); |
| rnsParcels[n].namespaceName = namespaceName; |
| rnsParcels[n].entries = new RequestEntryParcel[entryNames.size()]; |
| int m = 0; |
| for (String entryName : entryNames) { |
| rnsParcels[n].entries[m] = new RequestEntryParcel(); |
| rnsParcels[n].entries[m].name = entryName; |
| m++; |
| } |
| n++; |
| } |
| |
| GetEntriesResultParcel resultParcel = null; |
| try { |
| resultParcel = mBinder.getEntries( |
| requestMessage != null ? requestMessage : new byte[0], |
| rnsParcels, |
| sessionTranscript != null ? sessionTranscript : new byte[0], |
| readerSignature != null ? readerSignature : new byte[0], |
| mAllowUsingExhaustedKeys, |
| mAllowUsingExpiredKeys, |
| mIncrementKeyUsageCount); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| if (e.errorCode == ICredentialStore.ERROR_EPHEMERAL_PUBLIC_KEY_NOT_FOUND) { |
| throw new EphemeralPublicKeyNotFoundException(e.getMessage(), e); |
| } else if (e.errorCode == ICredentialStore.ERROR_INVALID_READER_SIGNATURE) { |
| throw new InvalidReaderSignatureException(e.getMessage(), e); |
| } else if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) { |
| throw new NoAuthenticationKeyAvailableException(e.getMessage(), e); |
| } else if (e.errorCode == ICredentialStore.ERROR_INVALID_ITEMS_REQUEST_MESSAGE) { |
| throw new InvalidRequestMessageException(e.getMessage(), e); |
| } else if (e.errorCode == ICredentialStore.ERROR_SESSION_TRANSCRIPT_MISMATCH) { |
| throw new SessionTranscriptMismatchException(e.getMessage(), e); |
| } else { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| byte[] signature = resultParcel.signature; |
| if (signature != null && signature.length == 0) { |
| signature = null; |
| } |
| |
| byte[] mac = resultParcel.mac; |
| if (mac != null && mac.length == 0) { |
| mac = null; |
| } |
| CredstoreResultData.Builder resultDataBuilder = new CredstoreResultData.Builder( |
| mFeatureVersion, resultParcel.staticAuthenticationData, |
| resultParcel.deviceNameSpaces, mac, signature); |
| |
| for (ResultNamespaceParcel resultNamespaceParcel : resultParcel.resultNamespaces) { |
| for (ResultEntryParcel resultEntryParcel : resultNamespaceParcel.entries) { |
| if (resultEntryParcel.status == ICredential.STATUS_OK) { |
| resultDataBuilder.addEntry(resultNamespaceParcel.namespaceName, |
| resultEntryParcel.name, resultEntryParcel.value); |
| } else { |
| resultDataBuilder.addErrorStatus(resultNamespaceParcel.namespaceName, |
| resultEntryParcel.name, |
| resultEntryParcel.status); |
| } |
| } |
| } |
| return resultDataBuilder.build(); |
| } |
| |
| @Override |
| public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) { |
| setAvailableAuthenticationKeys(keyCount, maxUsesPerKey, 0); |
| } |
| |
| @Override |
| public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey, |
| long minValidTimeMillis) { |
| try { |
| mBinder.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey, minValidTimeMillis); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| @Override |
| public @NonNull Collection<X509Certificate> getAuthKeysNeedingCertification() { |
| try { |
| AuthKeyParcel[] authKeyParcels = mBinder.getAuthKeysNeedingCertification(); |
| ArrayList<X509Certificate> x509Certs = new ArrayList<>(); |
| CertificateFactory factory = CertificateFactory.getInstance("X.509"); |
| for (AuthKeyParcel authKeyParcel : authKeyParcels) { |
| Collection<? extends Certificate> certs = null; |
| ByteArrayInputStream bais = new ByteArrayInputStream(authKeyParcel.x509cert); |
| certs = factory.generateCertificates(bais); |
| if (certs.size() != 1) { |
| throw new RuntimeException("Returned blob yields more than one X509 cert"); |
| } |
| X509Certificate authKeyCert = (X509Certificate) certs.iterator().next(); |
| x509Certs.add(authKeyCert); |
| } |
| return x509Certs; |
| } catch (CertificateException e) { |
| throw new RuntimeException("Error decoding authenticationKey", e); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| @Override |
| public void storeStaticAuthenticationData(X509Certificate authenticationKey, |
| byte[] staticAuthData) |
| throws UnknownAuthenticationKeyException { |
| try { |
| AuthKeyParcel authKeyParcel = new AuthKeyParcel(); |
| authKeyParcel.x509cert = authenticationKey.getEncoded(); |
| mBinder.storeStaticAuthenticationData(authKeyParcel, staticAuthData); |
| } catch (CertificateEncodingException e) { |
| throw new RuntimeException("Error encoding authenticationKey", e); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| if (e.errorCode == ICredentialStore.ERROR_AUTHENTICATION_KEY_NOT_FOUND) { |
| throw new UnknownAuthenticationKeyException(e.getMessage(), e); |
| } else { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| } |
| |
| @Override |
| public void storeStaticAuthenticationData(X509Certificate authenticationKey, |
| Instant expirationDate, |
| byte[] staticAuthData) |
| throws UnknownAuthenticationKeyException { |
| try { |
| AuthKeyParcel authKeyParcel = new AuthKeyParcel(); |
| authKeyParcel.x509cert = authenticationKey.getEncoded(); |
| long millisSinceEpoch = (expirationDate.getEpochSecond() * 1000) |
| + (expirationDate.getNano() / 1000000); |
| mBinder.storeStaticAuthenticationDataWithExpiration(authKeyParcel, |
| millisSinceEpoch, staticAuthData); |
| } catch (CertificateEncodingException e) { |
| throw new RuntimeException("Error encoding authenticationKey", e); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| if (e.errorCode == ICredentialStore.ERROR_NOT_SUPPORTED) { |
| throw new UnsupportedOperationException("Not supported", e); |
| } else if (e.errorCode == ICredentialStore.ERROR_AUTHENTICATION_KEY_NOT_FOUND) { |
| throw new UnknownAuthenticationKeyException(e.getMessage(), e); |
| } else { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| } |
| |
| @Override |
| public @NonNull int[] getAuthenticationDataUsageCount() { |
| try { |
| int[] usageCount = mBinder.getAuthenticationDataUsageCount(); |
| return usageCount; |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| @Override |
| public @NonNull List<AuthenticationKeyMetadata> getAuthenticationKeyMetadata() { |
| try { |
| int[] usageCount = mBinder.getAuthenticationDataUsageCount(); |
| long[] expirationsMillis = mBinder.getAuthenticationDataExpirations(); |
| if (usageCount.length != expirationsMillis.length) { |
| throw new IllegalStateException("Size og usageCount and expirationMillis differ"); |
| } |
| List<AuthenticationKeyMetadata> mds = new ArrayList<>(); |
| for (int n = 0; n < expirationsMillis.length; n++) { |
| AuthenticationKeyMetadata md = null; |
| long expirationMillis = expirationsMillis[n]; |
| if (expirationMillis != Long.MAX_VALUE) { |
| md = new AuthenticationKeyMetadata( |
| usageCount[n], |
| Instant.ofEpochMilli(expirationMillis)); |
| } |
| mds.add(md); |
| } |
| return mds; |
| } catch (android.os.RemoteException e) { |
| throw new IllegalStateException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new IllegalStateException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| @Override |
| public @NonNull byte[] proveOwnership(@NonNull byte[] challenge) { |
| try { |
| byte[] proofOfOwnership = mBinder.proveOwnership(challenge); |
| return proofOfOwnership; |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| if (e.errorCode == ICredentialStore.ERROR_NOT_SUPPORTED) { |
| throw new UnsupportedOperationException("Not supported", e); |
| } else { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| } |
| |
| @Override |
| public @NonNull byte[] delete(@NonNull byte[] challenge) { |
| try { |
| byte[] proofOfDeletion = mBinder.deleteWithChallenge(challenge); |
| return proofOfDeletion; |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| |
| @Override |
| public @NonNull byte[] update(@NonNull PersonalizationData personalizationData) { |
| try { |
| IWritableCredential binder = mBinder.update(); |
| byte[] proofOfProvision = |
| CredstoreWritableIdentityCredential.personalize(binder, personalizationData); |
| return proofOfProvision; |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| } |