1 /* 2 * Copyright 2019 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 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 23 import java.io.ByteArrayInputStream; 24 import java.io.IOException; 25 import java.nio.ByteBuffer; 26 import java.security.InvalidAlgorithmParameterException; 27 import java.security.InvalidKeyException; 28 import java.security.KeyPair; 29 import java.security.KeyStore; 30 import java.security.KeyStoreException; 31 import java.security.NoSuchAlgorithmException; 32 import java.security.PrivateKey; 33 import java.security.PublicKey; 34 import java.security.UnrecoverableKeyException; 35 import java.security.cert.Certificate; 36 import java.security.cert.CertificateEncodingException; 37 import java.security.cert.CertificateException; 38 import java.security.cert.CertificateFactory; 39 import java.security.cert.X509Certificate; 40 import java.time.Instant; 41 import java.util.Collection; 42 import java.util.LinkedList; 43 import java.util.Map; 44 45 import javax.crypto.BadPaddingException; 46 import javax.crypto.Cipher; 47 import javax.crypto.IllegalBlockSizeException; 48 import javax.crypto.KeyAgreement; 49 import javax.crypto.NoSuchPaddingException; 50 import javax.crypto.SecretKey; 51 import javax.crypto.spec.GCMParameterSpec; 52 import javax.crypto.spec.SecretKeySpec; 53 54 class CredstoreIdentityCredential extends IdentityCredential { 55 56 private static final String TAG = "CredstoreIdentityCredential"; 57 private String mCredentialName; 58 private @IdentityCredentialStore.Ciphersuite int mCipherSuite; 59 private Context mContext; 60 private ICredential mBinder; 61 CredstoreIdentityCredential(Context context, String credentialName, @IdentityCredentialStore.Ciphersuite int cipherSuite, ICredential binder)62 CredstoreIdentityCredential(Context context, String credentialName, 63 @IdentityCredentialStore.Ciphersuite int cipherSuite, 64 ICredential binder) { 65 mContext = context; 66 mCredentialName = credentialName; 67 mCipherSuite = cipherSuite; 68 mBinder = binder; 69 } 70 71 private KeyPair mEphemeralKeyPair = null; 72 private SecretKey mSecretKey = null; 73 private SecretKey mReaderSecretKey = null; 74 private int mEphemeralCounter; 75 private int mReadersExpectedEphemeralCounter; 76 ensureEphemeralKeyPair()77 private void ensureEphemeralKeyPair() { 78 if (mEphemeralKeyPair != null) { 79 return; 80 } 81 try { 82 // This PKCS#12 blob is generated in credstore, using BoringSSL. 83 // 84 // The main reason for this convoluted approach and not just sending the decomposed 85 // key-pair is that this would require directly using (device-side) BouncyCastle which 86 // is tricky due to various API hiding efforts. So instead we have credstore generate 87 // this PKCS#12 blob. The blob is encrypted with no password (sadly, also, BoringSSL 88 // doesn't support not using encryption when building a PKCS#12 blob). 89 // 90 byte[] pkcs12 = mBinder.createEphemeralKeyPair(); 91 String alias = "ephemeralKey"; 92 char[] password = {}; 93 94 KeyStore ks = KeyStore.getInstance("PKCS12"); 95 ByteArrayInputStream bais = new ByteArrayInputStream(pkcs12); 96 ks.load(bais, password); 97 PrivateKey privKey = (PrivateKey) ks.getKey(alias, password); 98 99 Certificate cert = ks.getCertificate(alias); 100 PublicKey pubKey = cert.getPublicKey(); 101 102 mEphemeralKeyPair = new KeyPair(pubKey, privKey); 103 } catch (android.os.RemoteException e) { 104 throw new RuntimeException("Unexpected RemoteException ", e); 105 } catch (android.os.ServiceSpecificException e) { 106 throw new RuntimeException("Unexpected ServiceSpecificException with code " 107 + e.errorCode, e); 108 } catch (KeyStoreException 109 | CertificateException 110 | UnrecoverableKeyException 111 | NoSuchAlgorithmException 112 | IOException e) { 113 throw new RuntimeException("Unexpected exception ", e); 114 } 115 } 116 117 @Override createEphemeralKeyPair()118 public @NonNull KeyPair createEphemeralKeyPair() { 119 ensureEphemeralKeyPair(); 120 return mEphemeralKeyPair; 121 } 122 123 @Override setReaderEphemeralPublicKey(@onNull PublicKey readerEphemeralPublicKey)124 public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) 125 throws InvalidKeyException { 126 try { 127 byte[] uncompressedForm = 128 Util.publicKeyEncodeUncompressedForm(readerEphemeralPublicKey); 129 mBinder.setReaderEphemeralPublicKey(uncompressedForm); 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 ensureEphemeralKeyPair(); 138 139 try { 140 KeyAgreement ka = KeyAgreement.getInstance("ECDH"); 141 ka.init(mEphemeralKeyPair.getPrivate()); 142 ka.doPhase(readerEphemeralPublicKey, true); 143 byte[] sharedSecret = ka.generateSecret(); 144 145 byte[] salt = new byte[1]; 146 byte[] info = new byte[0]; 147 148 salt[0] = 0x01; 149 byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32); 150 mSecretKey = new SecretKeySpec(derivedKey, "AES"); 151 152 salt[0] = 0x00; 153 derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32); 154 mReaderSecretKey = new SecretKeySpec(derivedKey, "AES"); 155 156 mEphemeralCounter = 1; 157 mReadersExpectedEphemeralCounter = 1; 158 159 } catch (NoSuchAlgorithmException e) { 160 throw new RuntimeException("Error performing key agreement", e); 161 } 162 } 163 164 @Override encryptMessageToReader(@onNull byte[] messagePlaintext)165 public @NonNull byte[] encryptMessageToReader(@NonNull byte[] messagePlaintext) { 166 byte[] messageCiphertextAndAuthTag = null; 167 try { 168 ByteBuffer iv = ByteBuffer.allocate(12); 169 iv.putInt(0, 0x00000000); 170 iv.putInt(4, 0x00000001); 171 iv.putInt(8, mEphemeralCounter); 172 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 173 GCMParameterSpec encryptionParameterSpec = new GCMParameterSpec(128, iv.array()); 174 cipher.init(Cipher.ENCRYPT_MODE, mSecretKey, encryptionParameterSpec); 175 messageCiphertextAndAuthTag = cipher.doFinal(messagePlaintext); 176 } catch (BadPaddingException 177 | IllegalBlockSizeException 178 | NoSuchPaddingException 179 | InvalidKeyException 180 | NoSuchAlgorithmException 181 | InvalidAlgorithmParameterException e) { 182 throw new RuntimeException("Error encrypting message", e); 183 } 184 mEphemeralCounter += 1; 185 return messageCiphertextAndAuthTag; 186 } 187 188 @Override decryptMessageFromReader(@onNull byte[] messageCiphertext)189 public @NonNull byte[] decryptMessageFromReader(@NonNull byte[] messageCiphertext) 190 throws MessageDecryptionException { 191 ByteBuffer iv = ByteBuffer.allocate(12); 192 iv.putInt(0, 0x00000000); 193 iv.putInt(4, 0x00000000); 194 iv.putInt(8, mReadersExpectedEphemeralCounter); 195 byte[] plainText = null; 196 try { 197 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 198 cipher.init(Cipher.DECRYPT_MODE, mReaderSecretKey, 199 new GCMParameterSpec(128, iv.array())); 200 plainText = cipher.doFinal(messageCiphertext); 201 } catch (BadPaddingException 202 | IllegalBlockSizeException 203 | InvalidAlgorithmParameterException 204 | InvalidKeyException 205 | NoSuchAlgorithmException 206 | NoSuchPaddingException e) { 207 throw new MessageDecryptionException("Error decrypting message", e); 208 } 209 mReadersExpectedEphemeralCounter += 1; 210 return plainText; 211 } 212 213 @Override getCredentialKeyCertificateChain()214 public @NonNull Collection<X509Certificate> getCredentialKeyCertificateChain() { 215 try { 216 byte[] certsBlob = mBinder.getCredentialKeyCertificateChain(); 217 ByteArrayInputStream bais = new ByteArrayInputStream(certsBlob); 218 219 Collection<? extends Certificate> certs = null; 220 try { 221 CertificateFactory factory = CertificateFactory.getInstance("X.509"); 222 certs = factory.generateCertificates(bais); 223 } catch (CertificateException e) { 224 throw new RuntimeException("Error decoding certificates", e); 225 } 226 227 LinkedList<X509Certificate> x509Certs = new LinkedList<>(); 228 for (Certificate cert : certs) { 229 x509Certs.add((X509Certificate) cert); 230 } 231 return x509Certs; 232 } catch (android.os.RemoteException e) { 233 throw new RuntimeException("Unexpected RemoteException ", e); 234 } catch (android.os.ServiceSpecificException e) { 235 throw new RuntimeException("Unexpected ServiceSpecificException with code " 236 + e.errorCode, e); 237 } 238 } 239 240 private boolean mAllowUsingExhaustedKeys = true; 241 private boolean mAllowUsingExpiredKeys = false; 242 243 @Override setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys)244 public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) { 245 mAllowUsingExhaustedKeys = allowUsingExhaustedKeys; 246 } 247 248 @Override setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys)249 public void setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) { 250 mAllowUsingExpiredKeys = allowUsingExpiredKeys; 251 } 252 253 private boolean mOperationHandleSet = false; 254 private long mOperationHandle = 0; 255 256 /** 257 * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an 258 * operation handle. 259 * 260 * @hide 261 */ 262 @Override getCredstoreOperationHandle()263 public long getCredstoreOperationHandle() { 264 if (!mOperationHandleSet) { 265 try { 266 mOperationHandle = mBinder.selectAuthKey(mAllowUsingExhaustedKeys, 267 mAllowUsingExpiredKeys); 268 mOperationHandleSet = true; 269 } catch (android.os.RemoteException e) { 270 throw new RuntimeException("Unexpected RemoteException ", e); 271 } catch (android.os.ServiceSpecificException e) { 272 if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) { 273 // The NoAuthenticationKeyAvailableException will be thrown when 274 // the caller proceeds to call getEntries(). 275 } 276 throw new RuntimeException("Unexpected ServiceSpecificException with code " 277 + e.errorCode, e); 278 } 279 } 280 return mOperationHandle; 281 } 282 283 @NonNull 284 @Override getEntries( @ullable byte[] requestMessage, @NonNull Map<String, Collection<String>> entriesToRequest, @Nullable byte[] sessionTranscript, @Nullable byte[] readerSignature)285 public ResultData getEntries( 286 @Nullable byte[] requestMessage, 287 @NonNull Map<String, Collection<String>> entriesToRequest, 288 @Nullable byte[] sessionTranscript, 289 @Nullable byte[] readerSignature) 290 throws SessionTranscriptMismatchException, NoAuthenticationKeyAvailableException, 291 InvalidReaderSignatureException, EphemeralPublicKeyNotFoundException, 292 InvalidRequestMessageException { 293 294 RequestNamespaceParcel[] rnsParcels = new RequestNamespaceParcel[entriesToRequest.size()]; 295 int n = 0; 296 for (String namespaceName : entriesToRequest.keySet()) { 297 Collection<String> entryNames = entriesToRequest.get(namespaceName); 298 rnsParcels[n] = new RequestNamespaceParcel(); 299 rnsParcels[n].namespaceName = namespaceName; 300 rnsParcels[n].entries = new RequestEntryParcel[entryNames.size()]; 301 int m = 0; 302 for (String entryName : entryNames) { 303 rnsParcels[n].entries[m] = new RequestEntryParcel(); 304 rnsParcels[n].entries[m].name = entryName; 305 m++; 306 } 307 n++; 308 } 309 310 GetEntriesResultParcel resultParcel = null; 311 try { 312 resultParcel = mBinder.getEntries( 313 requestMessage != null ? requestMessage : new byte[0], 314 rnsParcels, 315 sessionTranscript != null ? sessionTranscript : new byte[0], 316 readerSignature != null ? readerSignature : new byte[0], 317 mAllowUsingExhaustedKeys, 318 mAllowUsingExpiredKeys); 319 } catch (android.os.RemoteException e) { 320 throw new RuntimeException("Unexpected RemoteException ", e); 321 } catch (android.os.ServiceSpecificException e) { 322 if (e.errorCode == ICredentialStore.ERROR_EPHEMERAL_PUBLIC_KEY_NOT_FOUND) { 323 throw new EphemeralPublicKeyNotFoundException(e.getMessage(), e); 324 } else if (e.errorCode == ICredentialStore.ERROR_INVALID_READER_SIGNATURE) { 325 throw new InvalidReaderSignatureException(e.getMessage(), e); 326 } else if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) { 327 throw new NoAuthenticationKeyAvailableException(e.getMessage(), e); 328 } else if (e.errorCode == ICredentialStore.ERROR_INVALID_ITEMS_REQUEST_MESSAGE) { 329 throw new InvalidRequestMessageException(e.getMessage(), e); 330 } else if (e.errorCode == ICredentialStore.ERROR_SESSION_TRANSCRIPT_MISMATCH) { 331 throw new SessionTranscriptMismatchException(e.getMessage(), e); 332 } else { 333 throw new RuntimeException("Unexpected ServiceSpecificException with code " 334 + e.errorCode, e); 335 } 336 } 337 338 byte[] mac = resultParcel.mac; 339 if (mac != null && mac.length == 0) { 340 mac = null; 341 } 342 CredstoreResultData.Builder resultDataBuilder = new CredstoreResultData.Builder( 343 resultParcel.staticAuthenticationData, resultParcel.deviceNameSpaces, mac); 344 345 for (ResultNamespaceParcel resultNamespaceParcel : resultParcel.resultNamespaces) { 346 for (ResultEntryParcel resultEntryParcel : resultNamespaceParcel.entries) { 347 if (resultEntryParcel.status == ICredential.STATUS_OK) { 348 resultDataBuilder.addEntry(resultNamespaceParcel.namespaceName, 349 resultEntryParcel.name, resultEntryParcel.value); 350 } else { 351 resultDataBuilder.addErrorStatus(resultNamespaceParcel.namespaceName, 352 resultEntryParcel.name, 353 resultEntryParcel.status); 354 } 355 } 356 } 357 return resultDataBuilder.build(); 358 } 359 360 @Override setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey)361 public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) { 362 try { 363 mBinder.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey); 364 } catch (android.os.RemoteException e) { 365 throw new RuntimeException("Unexpected RemoteException ", e); 366 } catch (android.os.ServiceSpecificException e) { 367 throw new RuntimeException("Unexpected ServiceSpecificException with code " 368 + e.errorCode, e); 369 } 370 } 371 372 @Override getAuthKeysNeedingCertification()373 public @NonNull Collection<X509Certificate> getAuthKeysNeedingCertification() { 374 try { 375 AuthKeyParcel[] authKeyParcels = mBinder.getAuthKeysNeedingCertification(); 376 LinkedList<X509Certificate> x509Certs = new LinkedList<>(); 377 CertificateFactory factory = CertificateFactory.getInstance("X.509"); 378 for (AuthKeyParcel authKeyParcel : authKeyParcels) { 379 Collection<? extends Certificate> certs = null; 380 ByteArrayInputStream bais = new ByteArrayInputStream(authKeyParcel.x509cert); 381 certs = factory.generateCertificates(bais); 382 if (certs.size() != 1) { 383 throw new RuntimeException("Returned blob yields more than one X509 cert"); 384 } 385 X509Certificate authKeyCert = (X509Certificate) certs.iterator().next(); 386 x509Certs.add(authKeyCert); 387 } 388 return x509Certs; 389 } catch (CertificateException e) { 390 throw new RuntimeException("Error decoding authenticationKey", e); 391 } catch (android.os.RemoteException e) { 392 throw new RuntimeException("Unexpected RemoteException ", e); 393 } catch (android.os.ServiceSpecificException e) { 394 throw new RuntimeException("Unexpected ServiceSpecificException with code " 395 + e.errorCode, e); 396 } 397 } 398 399 @Override storeStaticAuthenticationData(X509Certificate authenticationKey, byte[] staticAuthData)400 public void storeStaticAuthenticationData(X509Certificate authenticationKey, 401 byte[] staticAuthData) 402 throws UnknownAuthenticationKeyException { 403 try { 404 AuthKeyParcel authKeyParcel = new AuthKeyParcel(); 405 authKeyParcel.x509cert = authenticationKey.getEncoded(); 406 mBinder.storeStaticAuthenticationData(authKeyParcel, staticAuthData); 407 } catch (CertificateEncodingException e) { 408 throw new RuntimeException("Error encoding authenticationKey", e); 409 } catch (android.os.RemoteException e) { 410 throw new RuntimeException("Unexpected RemoteException ", e); 411 } catch (android.os.ServiceSpecificException e) { 412 if (e.errorCode == ICredentialStore.ERROR_AUTHENTICATION_KEY_NOT_FOUND) { 413 throw new UnknownAuthenticationKeyException(e.getMessage(), e); 414 } else { 415 throw new RuntimeException("Unexpected ServiceSpecificException with code " 416 + e.errorCode, e); 417 } 418 } 419 } 420 421 @Override storeStaticAuthenticationData(X509Certificate authenticationKey, Instant expirationDate, byte[] staticAuthData)422 public void storeStaticAuthenticationData(X509Certificate authenticationKey, 423 Instant expirationDate, 424 byte[] staticAuthData) 425 throws UnknownAuthenticationKeyException { 426 try { 427 AuthKeyParcel authKeyParcel = new AuthKeyParcel(); 428 authKeyParcel.x509cert = authenticationKey.getEncoded(); 429 long millisSinceEpoch = (expirationDate.getEpochSecond() * 1000) 430 + (expirationDate.getNano() / 1000000); 431 mBinder.storeStaticAuthenticationDataWithExpiration(authKeyParcel, 432 millisSinceEpoch, staticAuthData); 433 } catch (CertificateEncodingException e) { 434 throw new RuntimeException("Error encoding authenticationKey", e); 435 } catch (android.os.RemoteException e) { 436 throw new RuntimeException("Unexpected RemoteException ", e); 437 } catch (android.os.ServiceSpecificException e) { 438 if (e.errorCode == ICredentialStore.ERROR_NOT_SUPPORTED) { 439 throw new UnsupportedOperationException("Not supported", e); 440 } else if (e.errorCode == ICredentialStore.ERROR_AUTHENTICATION_KEY_NOT_FOUND) { 441 throw new UnknownAuthenticationKeyException(e.getMessage(), e); 442 } else { 443 throw new RuntimeException("Unexpected ServiceSpecificException with code " 444 + e.errorCode, e); 445 } 446 } 447 } 448 449 @Override getAuthenticationDataUsageCount()450 public @NonNull int[] getAuthenticationDataUsageCount() { 451 try { 452 int[] usageCount = mBinder.getAuthenticationDataUsageCount(); 453 return usageCount; 454 } catch (android.os.RemoteException e) { 455 throw new RuntimeException("Unexpected RemoteException ", e); 456 } catch (android.os.ServiceSpecificException e) { 457 throw new RuntimeException("Unexpected ServiceSpecificException with code " 458 + e.errorCode, e); 459 } 460 } 461 462 @Override proveOwnership(@onNull byte[] challenge)463 public @NonNull byte[] proveOwnership(@NonNull byte[] challenge) { 464 try { 465 byte[] proofOfOwnership = mBinder.proveOwnership(challenge); 466 return proofOfOwnership; 467 } catch (android.os.RemoteException e) { 468 throw new RuntimeException("Unexpected RemoteException ", e); 469 } catch (android.os.ServiceSpecificException e) { 470 if (e.errorCode == ICredentialStore.ERROR_NOT_SUPPORTED) { 471 throw new UnsupportedOperationException("Not supported", e); 472 } else { 473 throw new RuntimeException("Unexpected ServiceSpecificException with code " 474 + e.errorCode, e); 475 } 476 } 477 } 478 479 @Override delete(@onNull byte[] challenge)480 public @NonNull byte[] delete(@NonNull byte[] challenge) { 481 try { 482 byte[] proofOfDeletion = mBinder.deleteWithChallenge(challenge); 483 return proofOfDeletion; 484 } catch (android.os.RemoteException e) { 485 throw new RuntimeException("Unexpected RemoteException ", e); 486 } catch (android.os.ServiceSpecificException e) { 487 throw new RuntimeException("Unexpected ServiceSpecificException with code " 488 + e.errorCode, e); 489 } 490 } 491 492 @Override update(@onNull PersonalizationData personalizationData)493 public @NonNull byte[] update(@NonNull PersonalizationData personalizationData) { 494 try { 495 IWritableCredential binder = mBinder.update(); 496 byte[] proofOfProvision = 497 CredstoreWritableIdentityCredential.personalize(binder, personalizationData); 498 return proofOfProvision; 499 } catch (android.os.RemoteException e) { 500 throw new RuntimeException("Unexpected RemoteException ", e); 501 } catch (android.os.ServiceSpecificException e) { 502 throw new RuntimeException("Unexpected ServiceSpecificException with code " 503 + e.errorCode, e); 504 } 505 } 506 } 507