1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.security.identity; 18 19 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.content.Context; 23 24 import java.io.ByteArrayInputStream; 25 import java.io.IOException; 26 import java.security.InvalidKeyException; 27 import java.security.KeyPair; 28 import java.security.KeyStore; 29 import java.security.KeyStoreException; 30 import java.security.NoSuchAlgorithmException; 31 import java.security.PrivateKey; 32 import java.security.PublicKey; 33 import java.security.UnrecoverableKeyException; 34 import java.security.cert.Certificate; 35 import java.security.cert.CertificateException; 36 import java.util.LinkedHashMap; 37 import java.util.Map; 38 39 class CredstorePresentationSession extends PresentationSession { 40 private static final String TAG = "CredstorePresentationSession"; 41 42 private @IdentityCredentialStore.Ciphersuite int mCipherSuite; 43 private Context mContext; 44 private CredstoreIdentityCredentialStore mStore; 45 private ISession mBinder; 46 private Map<String, CredstoreIdentityCredential> mCredentialCache = new LinkedHashMap<>(); 47 private KeyPair mEphemeralKeyPair = null; 48 private byte[] mSessionTranscript = null; 49 private boolean mOperationHandleSet = false; 50 private long mOperationHandle = 0; 51 private int mFeatureVersion = 0; 52 CredstorePresentationSession(Context context, @IdentityCredentialStore.Ciphersuite int cipherSuite, CredstoreIdentityCredentialStore store, ISession binder, int featureVersion)53 CredstorePresentationSession(Context context, 54 @IdentityCredentialStore.Ciphersuite int cipherSuite, 55 CredstoreIdentityCredentialStore store, 56 ISession binder, 57 int featureVersion) { 58 mContext = context; 59 mCipherSuite = cipherSuite; 60 mStore = store; 61 mBinder = binder; 62 mFeatureVersion = featureVersion; 63 } 64 ensureEphemeralKeyPair()65 private void ensureEphemeralKeyPair() { 66 if (mEphemeralKeyPair != null) { 67 return; 68 } 69 try { 70 // This PKCS#12 blob is generated in credstore, using BoringSSL. 71 // 72 // The main reason for this convoluted approach and not just sending the decomposed 73 // key-pair is that this would require directly using (device-side) BouncyCastle which 74 // is tricky due to various API hiding efforts. So instead we have credstore generate 75 // this PKCS#12 blob. The blob is encrypted with no password (sadly, also, BoringSSL 76 // doesn't support not using encryption when building a PKCS#12 blob). 77 // 78 byte[] pkcs12 = mBinder.getEphemeralKeyPair(); 79 String alias = "ephemeralKey"; 80 char[] password = {}; 81 82 KeyStore ks = KeyStore.getInstance("PKCS12"); 83 ByteArrayInputStream bais = new ByteArrayInputStream(pkcs12); 84 ks.load(bais, password); 85 PrivateKey privKey = (PrivateKey) ks.getKey(alias, password); 86 87 Certificate cert = ks.getCertificate(alias); 88 PublicKey pubKey = cert.getPublicKey(); 89 90 mEphemeralKeyPair = new KeyPair(pubKey, privKey); 91 } catch (android.os.ServiceSpecificException e) { 92 throw new RuntimeException("Unexpected ServiceSpecificException with code " 93 + e.errorCode, e); 94 } catch (android.os.RemoteException 95 | KeyStoreException 96 | CertificateException 97 | UnrecoverableKeyException 98 | NoSuchAlgorithmException 99 | IOException e) { 100 throw new RuntimeException("Unexpected exception ", e); 101 } 102 } 103 104 @Override getEphemeralKeyPair()105 public @NonNull KeyPair getEphemeralKeyPair() { 106 ensureEphemeralKeyPair(); 107 return mEphemeralKeyPair; 108 } 109 110 @Override setReaderEphemeralPublicKey(@onNull PublicKey readerEphemeralPublicKey)111 public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) 112 throws InvalidKeyException { 113 try { 114 byte[] uncompressedForm = 115 Util.publicKeyEncodeUncompressedForm(readerEphemeralPublicKey); 116 mBinder.setReaderEphemeralPublicKey(uncompressedForm); 117 } catch (android.os.RemoteException e) { 118 throw new RuntimeException("Unexpected RemoteException ", e); 119 } catch (android.os.ServiceSpecificException e) { 120 throw new RuntimeException("Unexpected ServiceSpecificException with code " 121 + e.errorCode, e); 122 } 123 } 124 125 @Override setSessionTranscript(@onNull byte[] sessionTranscript)126 public void setSessionTranscript(@NonNull byte[] sessionTranscript) { 127 try { 128 mBinder.setSessionTranscript(sessionTranscript); 129 mSessionTranscript = sessionTranscript; 130 } catch (android.os.RemoteException e) { 131 throw new RuntimeException("Unexpected RemoteException ", e); 132 } catch (android.os.ServiceSpecificException e) { 133 throw new RuntimeException("Unexpected ServiceSpecificException with code " 134 + e.errorCode, e); 135 } 136 } 137 138 @Override getCredentialData(@onNull String credentialName, @NonNull CredentialDataRequest request)139 public @Nullable CredentialDataResult getCredentialData(@NonNull String credentialName, 140 @NonNull CredentialDataRequest request) 141 throws NoAuthenticationKeyAvailableException, InvalidReaderSignatureException, 142 InvalidRequestMessageException, EphemeralPublicKeyNotFoundException { 143 try { 144 // Cache the IdentityCredential to satisfy the property that AuthKey usage counts are 145 // incremented on only the _first_ getCredentialData() call. 146 // 147 CredstoreIdentityCredential credential = mCredentialCache.get(credentialName); 148 if (credential == null) { 149 ICredential credstoreCredential = 150 mBinder.getCredentialForPresentation(credentialName); 151 credential = new CredstoreIdentityCredential(mContext, credentialName, 152 mCipherSuite, credstoreCredential, 153 this, mFeatureVersion); 154 mCredentialCache.put(credentialName, credential); 155 156 credential.setAllowUsingExhaustedKeys(request.isAllowUsingExhaustedKeys()); 157 credential.setAllowUsingExpiredKeys(request.isAllowUsingExpiredKeys()); 158 credential.setIncrementKeyUsageCount(request.isIncrementUseCount()); 159 } 160 161 ResultData deviceSignedResult = credential.getEntries( 162 request.getRequestMessage(), 163 request.getDeviceSignedEntriesToRequest(), 164 mSessionTranscript, 165 request.getReaderSignature()); 166 167 // By design this second getEntries() call consumes the same auth-key. 168 169 ResultData issuerSignedResult = credential.getEntries( 170 request.getRequestMessage(), 171 request.getIssuerSignedEntriesToRequest(), 172 mSessionTranscript, 173 request.getReaderSignature()); 174 175 return new CredstoreCredentialDataResult(deviceSignedResult, issuerSignedResult); 176 177 } catch (SessionTranscriptMismatchException e) { 178 throw new RuntimeException("Unexpected ", e); 179 } catch (android.os.RemoteException e) { 180 throw new RuntimeException("Unexpected RemoteException ", e); 181 } catch (android.os.ServiceSpecificException e) { 182 if (e.errorCode == ICredentialStore.ERROR_NO_SUCH_CREDENTIAL) { 183 return null; 184 } else { 185 throw new RuntimeException("Unexpected ServiceSpecificException with code " 186 + e.errorCode, e); 187 } 188 } 189 } 190 191 /** 192 * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an 193 * operation handle. 194 * 195 * @hide 196 */ 197 @Override getCredstoreOperationHandle()198 public long getCredstoreOperationHandle() { 199 if (!mOperationHandleSet) { 200 try { 201 mOperationHandle = mBinder.getAuthChallenge(); 202 mOperationHandleSet = true; 203 } catch (android.os.RemoteException e) { 204 throw new RuntimeException("Unexpected RemoteException ", e); 205 } catch (android.os.ServiceSpecificException e) { 206 if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) { 207 // The NoAuthenticationKeyAvailableException will be thrown when 208 // the caller proceeds to call getEntries(). 209 } 210 throw new RuntimeException("Unexpected ServiceSpecificException with code " 211 + e.errorCode, e); 212 } 213 } 214 return mOperationHandle; 215 } 216 217 } 218