| /* |
| * Copyright 2021 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.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.CertificateException; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| |
| class CredstorePresentationSession extends PresentationSession { |
| private static final String TAG = "CredstorePresentationSession"; |
| |
| private @IdentityCredentialStore.Ciphersuite int mCipherSuite; |
| private Context mContext; |
| private CredstoreIdentityCredentialStore mStore; |
| private ISession mBinder; |
| private Map<String, CredstoreIdentityCredential> mCredentialCache = new LinkedHashMap<>(); |
| private KeyPair mEphemeralKeyPair = null; |
| private byte[] mSessionTranscript = null; |
| private boolean mOperationHandleSet = false; |
| private long mOperationHandle = 0; |
| private int mFeatureVersion = 0; |
| |
| CredstorePresentationSession(Context context, |
| @IdentityCredentialStore.Ciphersuite int cipherSuite, |
| CredstoreIdentityCredentialStore store, |
| ISession binder, |
| int featureVersion) { |
| mContext = context; |
| mCipherSuite = cipherSuite; |
| mStore = store; |
| mBinder = binder; |
| mFeatureVersion = featureVersion; |
| } |
| |
| 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.getEphemeralKeyPair(); |
| 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.ServiceSpecificException e) { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } catch (android.os.RemoteException |
| | KeyStoreException |
| | CertificateException |
| | UnrecoverableKeyException |
| | NoSuchAlgorithmException |
| | IOException e) { |
| throw new RuntimeException("Unexpected exception ", e); |
| } |
| } |
| |
| @Override |
| public @NonNull KeyPair getEphemeralKeyPair() { |
| 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); |
| } |
| } |
| |
| @Override |
| public void setSessionTranscript(@NonNull byte[] sessionTranscript) { |
| try { |
| mBinder.setSessionTranscript(sessionTranscript); |
| mSessionTranscript = sessionTranscript; |
| } 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 @Nullable CredentialDataResult getCredentialData(@NonNull String credentialName, |
| @NonNull CredentialDataRequest request) |
| throws NoAuthenticationKeyAvailableException, InvalidReaderSignatureException, |
| InvalidRequestMessageException, EphemeralPublicKeyNotFoundException { |
| try { |
| // Cache the IdentityCredential to satisfy the property that AuthKey usage counts are |
| // incremented on only the _first_ getCredentialData() call. |
| // |
| CredstoreIdentityCredential credential = mCredentialCache.get(credentialName); |
| if (credential == null) { |
| ICredential credstoreCredential = |
| mBinder.getCredentialForPresentation(credentialName); |
| credential = new CredstoreIdentityCredential(mContext, credentialName, |
| mCipherSuite, credstoreCredential, |
| this, mFeatureVersion); |
| mCredentialCache.put(credentialName, credential); |
| |
| credential.setAllowUsingExhaustedKeys(request.isAllowUsingExhaustedKeys()); |
| credential.setAllowUsingExpiredKeys(request.isAllowUsingExpiredKeys()); |
| credential.setIncrementKeyUsageCount(request.isIncrementUseCount()); |
| } |
| |
| ResultData deviceSignedResult = credential.getEntries( |
| request.getRequestMessage(), |
| request.getDeviceSignedEntriesToRequest(), |
| mSessionTranscript, |
| request.getReaderSignature()); |
| |
| // By design this second getEntries() call consumes the same auth-key. |
| |
| ResultData issuerSignedResult = credential.getEntries( |
| request.getRequestMessage(), |
| request.getIssuerSignedEntriesToRequest(), |
| mSessionTranscript, |
| request.getReaderSignature()); |
| |
| return new CredstoreCredentialDataResult(deviceSignedResult, issuerSignedResult); |
| |
| } catch (SessionTranscriptMismatchException e) { |
| throw new RuntimeException("Unexpected ", e); |
| } catch (android.os.RemoteException e) { |
| throw new RuntimeException("Unexpected RemoteException ", e); |
| } catch (android.os.ServiceSpecificException e) { |
| if (e.errorCode == ICredentialStore.ERROR_NO_SUCH_CREDENTIAL) { |
| return null; |
| } else { |
| throw new RuntimeException("Unexpected ServiceSpecificException with code " |
| + e.errorCode, e); |
| } |
| } |
| } |
| |
| /** |
| * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an |
| * operation handle. |
| * |
| * @hide |
| */ |
| @Override |
| public long getCredstoreOperationHandle() { |
| if (!mOperationHandleSet) { |
| try { |
| mOperationHandle = mBinder.getAuthChallenge(); |
| 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; |
| } |
| |
| } |