1 /* 2 * Copyright (C) 2017 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 com.android.server.locksettings.recoverablekeystore; 18 19 import static android.security.keystore.recovery.RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT; 20 import static android.security.keystore.recovery.RecoveryController.ERROR_DECRYPTION_FAILED; 21 import static android.security.keystore.recovery.RecoveryController.ERROR_DOWNGRADE_CERTIFICATE; 22 import static android.security.keystore.recovery.RecoveryController.ERROR_INSECURE_USER; 23 import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_KEY_FORMAT; 24 import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE; 25 import static android.security.keystore.recovery.RecoveryController.ERROR_NO_SNAPSHOT_PENDING; 26 import static android.security.keystore.recovery.RecoveryController.ERROR_SERVICE_INTERNAL_ERROR; 27 import static android.security.keystore.recovery.RecoveryController.ERROR_SESSION_EXPIRED; 28 29 import android.Manifest; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.app.PendingIntent; 33 import android.content.Context; 34 import android.os.Binder; 35 import android.os.RemoteException; 36 import android.os.ServiceSpecificException; 37 import android.os.UserHandle; 38 import android.security.keystore.recovery.KeyChainProtectionParams; 39 import android.security.keystore.recovery.KeyChainSnapshot; 40 import android.security.keystore.recovery.RecoveryCertPath; 41 import android.security.keystore.recovery.RecoveryController; 42 import android.security.keystore.recovery.WrappedApplicationKey; 43 import android.security.KeyStore; 44 import android.util.ArrayMap; 45 import android.util.Log; 46 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.util.HexDump; 49 import com.android.internal.util.Preconditions; 50 import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException; 51 import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils; 52 import com.android.server.locksettings.recoverablekeystore.certificate.CertValidationException; 53 import com.android.server.locksettings.recoverablekeystore.certificate.CertXml; 54 import com.android.server.locksettings.recoverablekeystore.certificate.SigXml; 55 import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage; 56 import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; 57 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; 58 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; 59 60 import java.io.IOException; 61 import java.security.InvalidKeyException; 62 import java.security.KeyFactory; 63 import java.security.KeyStoreException; 64 import java.security.NoSuchAlgorithmException; 65 import java.security.PublicKey; 66 import java.security.SecureRandom; 67 import java.security.UnrecoverableKeyException; 68 import java.security.cert.CertPath; 69 import java.security.cert.CertificateEncodingException; 70 import java.security.cert.CertificateException; 71 import java.security.cert.X509Certificate; 72 import java.security.spec.InvalidKeySpecException; 73 import java.security.spec.X509EncodedKeySpec; 74 import java.util.Arrays; 75 import java.util.HashMap; 76 import java.util.List; 77 import java.util.Locale; 78 import java.util.Map; 79 import java.util.concurrent.ExecutorService; 80 import java.util.concurrent.Executors; 81 82 import javax.crypto.AEADBadTagException; 83 84 /** 85 * Class with {@link RecoveryController} API implementation and internal methods to interact 86 * with {@code LockSettingsService}. 87 * 88 * @hide 89 */ 90 public class RecoverableKeyStoreManager { 91 private static final String TAG = "RecoverableKeyStoreMgr"; 92 93 private static RecoverableKeyStoreManager mInstance; 94 95 private final Context mContext; 96 private final RecoverableKeyStoreDb mDatabase; 97 private final RecoverySessionStorage mRecoverySessionStorage; 98 private final ExecutorService mExecutorService; 99 private final RecoverySnapshotListenersStorage mListenersStorage; 100 private final RecoverableKeyGenerator mRecoverableKeyGenerator; 101 private final RecoverySnapshotStorage mSnapshotStorage; 102 private final PlatformKeyManager mPlatformKeyManager; 103 private final ApplicationKeyStorage mApplicationKeyStorage; 104 private final TestOnlyInsecureCertificateHelper mTestCertHelper; 105 106 /** 107 * Returns a new or existing instance. 108 * 109 * @hide 110 */ 111 public static synchronized RecoverableKeyStoreManager 112 getInstance(Context context, KeyStore keystore) { 113 if (mInstance == null) { 114 RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(context); 115 PlatformKeyManager platformKeyManager; 116 ApplicationKeyStorage applicationKeyStorage; 117 try { 118 platformKeyManager = PlatformKeyManager.getInstance(context, db); 119 applicationKeyStorage = ApplicationKeyStorage.getInstance(keystore); 120 } catch (NoSuchAlgorithmException e) { 121 // Impossible: all algorithms must be supported by AOSP 122 throw new RuntimeException(e); 123 } catch (KeyStoreException e) { 124 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 125 } 126 127 mInstance = new RecoverableKeyStoreManager( 128 context.getApplicationContext(), 129 db, 130 new RecoverySessionStorage(), 131 Executors.newSingleThreadExecutor(), 132 RecoverySnapshotStorage.newInstance(), 133 new RecoverySnapshotListenersStorage(), 134 platformKeyManager, 135 applicationKeyStorage, 136 new TestOnlyInsecureCertificateHelper()); 137 } 138 return mInstance; 139 } 140 141 @VisibleForTesting 142 RecoverableKeyStoreManager( 143 Context context, 144 RecoverableKeyStoreDb recoverableKeyStoreDb, 145 RecoverySessionStorage recoverySessionStorage, 146 ExecutorService executorService, 147 RecoverySnapshotStorage snapshotStorage, 148 RecoverySnapshotListenersStorage listenersStorage, 149 PlatformKeyManager platformKeyManager, 150 ApplicationKeyStorage applicationKeyStorage, 151 TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) { 152 mContext = context; 153 mDatabase = recoverableKeyStoreDb; 154 mRecoverySessionStorage = recoverySessionStorage; 155 mExecutorService = executorService; 156 mListenersStorage = listenersStorage; 157 mSnapshotStorage = snapshotStorage; 158 mPlatformKeyManager = platformKeyManager; 159 mApplicationKeyStorage = applicationKeyStorage; 160 mTestCertHelper = TestOnlyInsecureCertificateHelper; 161 162 try { 163 mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase); 164 } catch (NoSuchAlgorithmException e) { 165 Log.wtf(TAG, "AES keygen algorithm not available. AOSP must support this.", e); 166 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 167 } 168 } 169 170 /** 171 * Used by {@link #initRecoveryServiceWithSigFile(String, byte[], byte[])}. 172 */ 173 @VisibleForTesting 174 void initRecoveryService( 175 @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile) 176 throws RemoteException { 177 checkRecoverKeyStorePermission(); 178 int userId = UserHandle.getCallingUserId(); 179 int uid = Binder.getCallingUid(); 180 181 rootCertificateAlias 182 = mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias); 183 if (!mTestCertHelper.isValidRootCertificateAlias(rootCertificateAlias)) { 184 throw new ServiceSpecificException( 185 ERROR_INVALID_CERTIFICATE, "Invalid root certificate alias"); 186 } 187 // Always set active alias to the argument of the last call to initRecoveryService method, 188 // even if cert file is incorrect. 189 String activeRootAlias = mDatabase.getActiveRootOfTrust(userId, uid); 190 if (activeRootAlias == null) { 191 Log.d(TAG, "Root of trust for recovery agent + " + uid 192 + " is assigned for the first time to " + rootCertificateAlias); 193 } else if (!activeRootAlias.equals(rootCertificateAlias)) { 194 Log.i(TAG, "Root of trust for recovery agent " + uid + " is changed to " 195 + rootCertificateAlias + " from " + activeRootAlias); 196 } 197 long updatedRows = mDatabase.setActiveRootOfTrust(userId, uid, rootCertificateAlias); 198 if (updatedRows < 0) { 199 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, 200 "Failed to set the root of trust in the local DB."); 201 } 202 203 CertXml certXml; 204 try { 205 certXml = CertXml.parse(recoveryServiceCertFile); 206 } catch (CertParsingException e) { 207 Log.d(TAG, "Failed to parse the input as a cert file: " + HexDump.toHexString( 208 recoveryServiceCertFile)); 209 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage()); 210 } 211 212 // Check serial number 213 long newSerial = certXml.getSerial(); 214 Long oldSerial = mDatabase.getRecoveryServiceCertSerial(userId, uid, rootCertificateAlias); 215 if (oldSerial != null && oldSerial >= newSerial 216 && !mTestCertHelper.isTestOnlyCertificateAlias(rootCertificateAlias)) { 217 if (oldSerial == newSerial) { 218 Log.i(TAG, "The cert file serial number is the same, so skip updating."); 219 } else { 220 Log.e(TAG, "The cert file serial number is older than the one in database."); 221 throw new ServiceSpecificException(ERROR_DOWNGRADE_CERTIFICATE, 222 "The cert file serial number is older than the one in database."); 223 } 224 return; 225 } 226 Log.i(TAG, "Updating the certificate with the new serial number " + newSerial); 227 228 // Randomly choose and validate an endpoint certificate from the list 229 CertPath certPath; 230 X509Certificate rootCert = 231 mTestCertHelper.getRootCertificate(rootCertificateAlias); 232 try { 233 Log.d(TAG, "Getting and validating a random endpoint certificate"); 234 certPath = certXml.getRandomEndpointCert(rootCert); 235 } catch (CertValidationException e) { 236 Log.e(TAG, "Invalid endpoint cert", e); 237 throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage()); 238 } 239 240 // Save the chosen and validated certificate into database 241 try { 242 Log.d(TAG, "Saving the randomly chosen endpoint certificate to database"); 243 long updatedCertPathRows = mDatabase.setRecoveryServiceCertPath(userId, uid, 244 rootCertificateAlias, certPath); 245 if (updatedCertPathRows > 0) { 246 long updatedCertSerialRows = mDatabase.setRecoveryServiceCertSerial(userId, uid, 247 rootCertificateAlias, newSerial); 248 if (updatedCertSerialRows < 0) { 249 // Ideally CertPath and CertSerial should be updated together in single 250 // transaction, but since their mismatch doesn't create many problems 251 // extra complexity is unnecessary. 252 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, 253 "Failed to set the certificate serial number in the local DB."); 254 } 255 if (mDatabase.getSnapshotVersion(userId, uid) != null) { 256 mDatabase.setShouldCreateSnapshot(userId, uid, true); 257 Log.i(TAG, "This is a certificate change. Snapshot must be updated"); 258 } else { 259 Log.i(TAG, "This is a certificate change. Snapshot didn't exist"); 260 } 261 long updatedCounterIdRows = 262 mDatabase.setCounterId(userId, uid, new SecureRandom().nextLong()); 263 if (updatedCounterIdRows < 0) { 264 Log.e(TAG, "Failed to set the counter id in the local DB."); 265 } 266 } else if (updatedCertPathRows < 0) { 267 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, 268 "Failed to set the certificate path in the local DB."); 269 } 270 } catch (CertificateEncodingException e) { 271 Log.e(TAG, "Failed to encode CertPath", e); 272 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage()); 273 } 274 } 275 276 /** 277 * Initializes the recovery service with the two files {@code recoveryServiceCertFile} and 278 * {@code recoveryServiceSigFile}. 279 * 280 * @param rootCertificateAlias the alias for the root certificate that is used for validating 281 * the recovery service certificates. 282 * @param recoveryServiceCertFile the content of the XML file containing a list of certificates 283 * for the recovery service. 284 * @param recoveryServiceSigFile the content of the XML file containing the public-key signature 285 * over the entire content of {@code recoveryServiceCertFile}. 286 */ 287 public void initRecoveryServiceWithSigFile( 288 @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile, 289 @NonNull byte[] recoveryServiceSigFile) 290 throws RemoteException { 291 checkRecoverKeyStorePermission(); 292 rootCertificateAlias = 293 mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias); 294 Preconditions.checkNotNull(recoveryServiceCertFile, "recoveryServiceCertFile is null"); 295 Preconditions.checkNotNull(recoveryServiceSigFile, "recoveryServiceSigFile is null"); 296 297 SigXml sigXml; 298 try { 299 sigXml = SigXml.parse(recoveryServiceSigFile); 300 } catch (CertParsingException e) { 301 Log.d(TAG, "Failed to parse the sig file: " + HexDump.toHexString( 302 recoveryServiceSigFile)); 303 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage()); 304 } 305 306 X509Certificate rootCert = 307 mTestCertHelper.getRootCertificate(rootCertificateAlias); 308 try { 309 sigXml.verifyFileSignature(rootCert, recoveryServiceCertFile); 310 } catch (CertValidationException e) { 311 Log.d(TAG, "The signature over the cert file is invalid." 312 + " Cert: " + HexDump.toHexString(recoveryServiceCertFile) 313 + " Sig: " + HexDump.toHexString(recoveryServiceSigFile)); 314 throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage()); 315 } 316 317 initRecoveryService(rootCertificateAlias, recoveryServiceCertFile); 318 } 319 320 /** 321 * Gets all data necessary to recover application keys on new device. 322 * 323 * @return KeyChain Snapshot. 324 * @throws ServiceSpecificException if no snapshot is pending. 325 * @hide 326 */ 327 public @NonNull KeyChainSnapshot getKeyChainSnapshot() 328 throws RemoteException { 329 checkRecoverKeyStorePermission(); 330 int uid = Binder.getCallingUid(); 331 KeyChainSnapshot snapshot = mSnapshotStorage.get(uid); 332 if (snapshot == null) { 333 throw new ServiceSpecificException(ERROR_NO_SNAPSHOT_PENDING); 334 } 335 return snapshot; 336 } 337 338 public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) 339 throws RemoteException { 340 checkRecoverKeyStorePermission(); 341 int uid = Binder.getCallingUid(); 342 mListenersStorage.setSnapshotListener(uid, intent); 343 } 344 345 /** 346 * Set the server params for the user's key chain. This is used to uniquely identify a key 347 * chain. Along with the counter ID, it is used to uniquely identify an instance of a vault. 348 */ 349 public void setServerParams(@NonNull byte[] serverParams) throws RemoteException { 350 checkRecoverKeyStorePermission(); 351 int userId = UserHandle.getCallingUserId(); 352 int uid = Binder.getCallingUid(); 353 354 byte[] currentServerParams = mDatabase.getServerParams(userId, uid); 355 356 if (Arrays.equals(serverParams, currentServerParams)) { 357 Log.v(TAG, "Not updating server params - same as old value."); 358 return; 359 } 360 361 long updatedRows = mDatabase.setServerParams(userId, uid, serverParams); 362 if (updatedRows < 0) { 363 throw new ServiceSpecificException( 364 ERROR_SERVICE_INTERNAL_ERROR, "Database failure trying to set server params."); 365 } 366 367 if (currentServerParams == null) { 368 Log.i(TAG, "Initialized server params."); 369 return; 370 } 371 372 if (mDatabase.getSnapshotVersion(userId, uid) != null) { 373 mDatabase.setShouldCreateSnapshot(userId, uid, true); 374 Log.i(TAG, "Updated server params. Snapshot must be updated"); 375 } else { 376 Log.i(TAG, "Updated server params. Snapshot didn't exist"); 377 } 378 } 379 380 /** 381 * Sets the recovery status of key with {@code alias} to {@code status}. 382 */ 383 public void setRecoveryStatus(@NonNull String alias, int status) throws RemoteException { 384 checkRecoverKeyStorePermission(); 385 Preconditions.checkNotNull(alias, "alias is null"); 386 long updatedRows = mDatabase.setRecoveryStatus(Binder.getCallingUid(), alias, status); 387 if (updatedRows < 0) { 388 throw new ServiceSpecificException( 389 ERROR_SERVICE_INTERNAL_ERROR, 390 "Failed to set the key recovery status in the local DB."); 391 } 392 } 393 394 /** 395 * Returns recovery statuses for all keys belonging to the calling uid. 396 * 397 * @return {@link Map} from key alias to recovery status. Recovery status is one of 398 * {@link RecoveryController#RECOVERY_STATUS_SYNCED}, 399 * {@link RecoveryController#RECOVERY_STATUS_SYNC_IN_PROGRESS} or 400 * {@link RecoveryController#RECOVERY_STATUS_PERMANENT_FAILURE}. 401 */ 402 public @NonNull Map<String, Integer> getRecoveryStatus() throws RemoteException { 403 checkRecoverKeyStorePermission(); 404 return mDatabase.getStatusForAllKeys(Binder.getCallingUid()); 405 } 406 407 /** 408 * Sets recovery secrets list used by all recovery agents for given {@code userId} 409 * 410 * @hide 411 */ 412 public void setRecoverySecretTypes( 413 @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes) 414 throws RemoteException { 415 checkRecoverKeyStorePermission(); 416 Preconditions.checkNotNull(secretTypes, "secretTypes is null"); 417 int userId = UserHandle.getCallingUserId(); 418 int uid = Binder.getCallingUid(); 419 420 int[] currentSecretTypes = mDatabase.getRecoverySecretTypes(userId, uid); 421 if (Arrays.equals(secretTypes, currentSecretTypes)) { 422 Log.v(TAG, "Not updating secret types - same as old value."); 423 return; 424 } 425 426 long updatedRows = mDatabase.setRecoverySecretTypes(userId, uid, secretTypes); 427 if (updatedRows < 0) { 428 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, 429 "Database error trying to set secret types."); 430 } 431 432 if (currentSecretTypes.length == 0) { 433 Log.i(TAG, "Initialized secret types."); 434 return; 435 } 436 437 Log.i(TAG, "Updated secret types. Snapshot pending."); 438 if (mDatabase.getSnapshotVersion(userId, uid) != null) { 439 mDatabase.setShouldCreateSnapshot(userId, uid, true); 440 Log.i(TAG, "Updated secret types. Snapshot must be updated"); 441 } else { 442 Log.i(TAG, "Updated secret types. Snapshot didn't exist"); 443 } 444 } 445 446 /** 447 * Gets secret types necessary to create Recovery Data. 448 * 449 * @return secret types 450 * @hide 451 */ 452 public @NonNull int[] getRecoverySecretTypes() throws RemoteException { 453 checkRecoverKeyStorePermission(); 454 return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(), 455 Binder.getCallingUid()); 456 } 457 458 /** 459 * Initializes recovery session given the X509-encoded public key of the recovery service. 460 * 461 * @param sessionId A unique ID to identify the recovery session. 462 * @param verifierPublicKey X509-encoded public key. 463 * @param vaultParams Additional params associated with vault. 464 * @param vaultChallenge Challenge issued by vault service. 465 * @param secrets Lock-screen hashes. For now only a single secret is supported. 466 * @return Encrypted bytes of recovery claim. This can then be issued to the vault service. 467 * @deprecated Use {@link #startRecoverySessionWithCertPath(String, String, RecoveryCertPath, 468 * byte[], byte[], List)} instead. 469 * 470 * @hide 471 */ 472 @VisibleForTesting 473 @NonNull byte[] startRecoverySession( 474 @NonNull String sessionId, 475 @NonNull byte[] verifierPublicKey, 476 @NonNull byte[] vaultParams, 477 @NonNull byte[] vaultChallenge, 478 @NonNull List<KeyChainProtectionParams> secrets) 479 throws RemoteException { 480 checkRecoverKeyStorePermission(); 481 int uid = Binder.getCallingUid(); 482 483 if (secrets.size() != 1) { 484 throw new UnsupportedOperationException( 485 "Only a single KeyChainProtectionParams is supported"); 486 } 487 488 PublicKey publicKey; 489 try { 490 publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey); 491 } catch (InvalidKeySpecException e) { 492 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage()); 493 } 494 // The raw public key bytes contained in vaultParams must match the ones given in 495 // verifierPublicKey; otherwise, the user secret may be decrypted by a key that is not owned 496 // by the original recovery service. 497 if (!publicKeysMatch(publicKey, vaultParams)) { 498 throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, 499 "The public keys given in verifierPublicKey and vaultParams do not match."); 500 } 501 502 byte[] keyClaimant = KeySyncUtils.generateKeyClaimant(); 503 byte[] kfHash = secrets.get(0).getSecret(); 504 mRecoverySessionStorage.add( 505 uid, 506 new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams)); 507 508 Log.i(TAG, "Received VaultParams for recovery: " + HexDump.toHexString(vaultParams)); 509 try { 510 byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash); 511 return KeySyncUtils.encryptRecoveryClaim( 512 publicKey, 513 vaultParams, 514 vaultChallenge, 515 thmKfHash, 516 keyClaimant); 517 } catch (NoSuchAlgorithmException e) { 518 Log.wtf(TAG, "SecureBox algorithm missing. AOSP must support this.", e); 519 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 520 } catch (InvalidKeyException e) { 521 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage()); 522 } 523 } 524 525 /** 526 * Initializes recovery session given the certificate path of the recovery service. 527 * 528 * @param sessionId A unique ID to identify the recovery session. 529 * @param verifierCertPath The certificate path of the recovery service. 530 * @param vaultParams Additional params associated with vault. 531 * @param vaultChallenge Challenge issued by vault service. 532 * @param secrets Lock-screen hashes. For now only a single secret is supported. 533 * @return Encrypted bytes of recovery claim. This can then be issued to the vault service. 534 * 535 * @hide 536 */ 537 public @NonNull byte[] startRecoverySessionWithCertPath( 538 @NonNull String sessionId, 539 @NonNull String rootCertificateAlias, 540 @NonNull RecoveryCertPath verifierCertPath, 541 @NonNull byte[] vaultParams, 542 @NonNull byte[] vaultChallenge, 543 @NonNull List<KeyChainProtectionParams> secrets) 544 throws RemoteException { 545 checkRecoverKeyStorePermission(); 546 rootCertificateAlias = 547 mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias); 548 Preconditions.checkNotNull(sessionId, "invalid session"); 549 Preconditions.checkNotNull(verifierCertPath, "verifierCertPath is null"); 550 Preconditions.checkNotNull(vaultParams, "vaultParams is null"); 551 Preconditions.checkNotNull(vaultChallenge, "vaultChallenge is null"); 552 Preconditions.checkNotNull(secrets, "secrets is null"); 553 CertPath certPath; 554 try { 555 certPath = verifierCertPath.getCertPath(); 556 } catch (CertificateException e) { 557 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage()); 558 } 559 560 try { 561 CertUtils.validateCertPath( 562 mTestCertHelper.getRootCertificate(rootCertificateAlias), certPath); 563 } catch (CertValidationException e) { 564 Log.e(TAG, "Failed to validate the given cert path", e); 565 throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage()); 566 } 567 568 byte[] verifierPublicKey = certPath.getCertificates().get(0).getPublicKey().getEncoded(); 569 if (verifierPublicKey == null) { 570 Log.e(TAG, "Failed to encode verifierPublicKey"); 571 throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, 572 "Failed to encode verifierPublicKey"); 573 } 574 575 return startRecoverySession( 576 sessionId, verifierPublicKey, vaultParams, vaultChallenge, secrets); 577 } 578 579 /** 580 * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault 581 * service. 582 * 583 * @param sessionId The session ID used to generate the claim. See 584 * {@link #startRecoverySession(String, byte[], byte[], byte[], List)}. 585 * @param encryptedRecoveryKey The encrypted recovery key blob returned by the remote vault 586 * service. 587 * @param applicationKeys The encrypted key blobs returned by the remote vault service. These 588 * were wrapped with the recovery key. 589 * @throws RemoteException if an error occurred recovering the keys. 590 */ 591 public @NonNull Map<String, String> recoverKeyChainSnapshot( 592 @NonNull String sessionId, 593 @NonNull byte[] encryptedRecoveryKey, 594 @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException { 595 checkRecoverKeyStorePermission(); 596 int userId = UserHandle.getCallingUserId(); 597 int uid = Binder.getCallingUid(); 598 RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId); 599 if (sessionEntry == null) { 600 throw new ServiceSpecificException(ERROR_SESSION_EXPIRED, 601 String.format(Locale.US, 602 "Application uid=%d does not have pending session '%s'", 603 uid, 604 sessionId)); 605 } 606 607 try { 608 byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey); 609 Map<String, byte[]> keysByAlias = recoverApplicationKeys(recoveryKey, applicationKeys); 610 return importKeyMaterials(userId, uid, keysByAlias); 611 } catch (KeyStoreException e) { 612 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 613 } finally { 614 sessionEntry.destroy(); 615 mRecoverySessionStorage.remove(uid); 616 } 617 } 618 619 /** 620 * Imports the key materials, returning a map from alias to grant alias for the calling user. 621 * 622 * @param userId The calling user ID. 623 * @param uid The calling uid. 624 * @param keysByAlias The key materials, keyed by alias. 625 * @throws KeyStoreException if an error occurs importing the key or getting the grant. 626 */ 627 private @NonNull Map<String, String> importKeyMaterials( 628 int userId, int uid, Map<String, byte[]> keysByAlias) throws KeyStoreException { 629 ArrayMap<String, String> grantAliasesByAlias = new ArrayMap<>(keysByAlias.size()); 630 for (String alias : keysByAlias.keySet()) { 631 mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keysByAlias.get(alias)); 632 String grantAlias = getAlias(userId, uid, alias); 633 Log.i(TAG, String.format(Locale.US, "Import %s -> %s", alias, grantAlias)); 634 grantAliasesByAlias.put(alias, grantAlias); 635 } 636 return grantAliasesByAlias; 637 } 638 639 /** 640 * Returns an alias for the key. 641 * 642 * @param userId The user ID of the calling process. 643 * @param uid The uid of the calling process. 644 * @param alias The alias of the key. 645 * @return The alias in the calling process's keystore. 646 */ 647 private @Nullable String getAlias(int userId, int uid, String alias) { 648 return mApplicationKeyStorage.getGrantAlias(userId, uid, alias); 649 } 650 651 /** 652 * Destroys the session with the given {@code sessionId}. 653 */ 654 public void closeSession(@NonNull String sessionId) throws RemoteException { 655 checkRecoverKeyStorePermission(); 656 Preconditions.checkNotNull(sessionId, "invalid session"); 657 mRecoverySessionStorage.remove(Binder.getCallingUid(), sessionId); 658 } 659 660 public void removeKey(@NonNull String alias) throws RemoteException { 661 checkRecoverKeyStorePermission(); 662 Preconditions.checkNotNull(alias, "alias is null"); 663 int uid = Binder.getCallingUid(); 664 int userId = UserHandle.getCallingUserId(); 665 666 boolean wasRemoved = mDatabase.removeKey(uid, alias); 667 if (wasRemoved) { 668 mDatabase.setShouldCreateSnapshot(userId, uid, true); 669 mApplicationKeyStorage.deleteEntry(userId, uid, alias); 670 } 671 } 672 673 /** 674 * Generates a key named {@code alias} in caller's namespace. 675 * The key is stored in system service keystore namespace. 676 * 677 * @return grant alias, which caller can use to access the key. 678 */ 679 public String generateKey(@NonNull String alias) throws RemoteException { 680 checkRecoverKeyStorePermission(); 681 Preconditions.checkNotNull(alias, "alias is null"); 682 int uid = Binder.getCallingUid(); 683 int userId = UserHandle.getCallingUserId(); 684 685 PlatformEncryptionKey encryptionKey; 686 try { 687 encryptionKey = mPlatformKeyManager.getEncryptKey(userId); 688 } catch (NoSuchAlgorithmException e) { 689 // Impossible: all algorithms must be supported by AOSP 690 throw new RuntimeException(e); 691 } catch (KeyStoreException | UnrecoverableKeyException | IOException e) { 692 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 693 } catch (InsecureUserException e) { 694 throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage()); 695 } 696 697 try { 698 byte[] secretKey = 699 mRecoverableKeyGenerator.generateAndStoreKey(encryptionKey, userId, uid, alias); 700 mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, secretKey); 701 return getAlias(userId, uid, alias); 702 } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) { 703 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 704 } 705 } 706 707 /** 708 * Imports a 256-bit AES-GCM key named {@code alias}. The key is stored in system service 709 * keystore namespace. 710 * 711 * @param alias the alias provided by caller as a reference to the key. 712 * @param keyBytes the raw bytes of the 256-bit AES key. 713 * @return grant alias, which caller can use to access the key. 714 * @throws RemoteException if the given key is invalid or some internal errors occur. 715 * 716 * @hide 717 */ 718 public @Nullable String importKey(@NonNull String alias, @NonNull byte[] keyBytes) 719 throws RemoteException { 720 checkRecoverKeyStorePermission(); 721 Preconditions.checkNotNull(alias, "alias is null"); 722 Preconditions.checkNotNull(keyBytes, "keyBytes is null"); 723 if (keyBytes.length != RecoverableKeyGenerator.KEY_SIZE_BITS / Byte.SIZE) { 724 Log.e(TAG, "The given key for import doesn't have the required length " 725 + RecoverableKeyGenerator.KEY_SIZE_BITS); 726 throw new ServiceSpecificException(ERROR_INVALID_KEY_FORMAT, 727 "The given key does not contain " + RecoverableKeyGenerator.KEY_SIZE_BITS 728 + " bits."); 729 } 730 731 int uid = Binder.getCallingUid(); 732 int userId = UserHandle.getCallingUserId(); 733 734 PlatformEncryptionKey encryptionKey; 735 try { 736 encryptionKey = mPlatformKeyManager.getEncryptKey(userId); 737 } catch (NoSuchAlgorithmException e) { 738 // Impossible: all algorithms must be supported by AOSP 739 throw new RuntimeException(e); 740 } catch (KeyStoreException | UnrecoverableKeyException | IOException e) { 741 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 742 } catch (InsecureUserException e) { 743 throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage()); 744 } 745 746 try { 747 // Wrap the key by the platform key and store the wrapped key locally 748 mRecoverableKeyGenerator.importKey(encryptionKey, userId, uid, alias, keyBytes); 749 750 // Import the key to Android KeyStore and get grant 751 mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keyBytes); 752 return getAlias(userId, uid, alias); 753 } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) { 754 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 755 } 756 } 757 758 /** 759 * Gets a key named {@code alias} in caller's namespace. 760 * 761 * @return grant alias, which caller can use to access the key. 762 */ 763 public @Nullable String getKey(@NonNull String alias) throws RemoteException { 764 checkRecoverKeyStorePermission(); 765 Preconditions.checkNotNull(alias, "alias is null"); 766 int uid = Binder.getCallingUid(); 767 int userId = UserHandle.getCallingUserId(); 768 return getAlias(userId, uid, alias); 769 } 770 771 private byte[] decryptRecoveryKey( 772 RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse) 773 throws RemoteException, ServiceSpecificException { 774 byte[] locallyEncryptedKey; 775 try { 776 locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse( 777 sessionEntry.getKeyClaimant(), 778 sessionEntry.getVaultParams(), 779 encryptedClaimResponse); 780 } catch (InvalidKeyException e) { 781 Log.e(TAG, "Got InvalidKeyException during decrypting recovery claim response", e); 782 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED, 783 "Failed to decrypt recovery key " + e.getMessage()); 784 } catch (AEADBadTagException e) { 785 Log.e(TAG, "Got AEADBadTagException during decrypting recovery claim response", e); 786 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED, 787 "Failed to decrypt recovery key " + e.getMessage()); 788 } catch (NoSuchAlgorithmException e) { 789 // Should never happen: all the algorithms used are required by AOSP implementations 790 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 791 } 792 793 try { 794 return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey); 795 } catch (InvalidKeyException e) { 796 Log.e(TAG, "Got InvalidKeyException during decrypting recovery key", e); 797 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED, 798 "Failed to decrypt recovery key " + e.getMessage()); 799 } catch (AEADBadTagException e) { 800 Log.e(TAG, "Got AEADBadTagException during decrypting recovery key", e); 801 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED, 802 "Failed to decrypt recovery key " + e.getMessage()); 803 } catch (NoSuchAlgorithmException e) { 804 // Should never happen: all the algorithms used are required by AOSP implementations 805 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 806 } 807 } 808 809 /** 810 * Uses {@code recoveryKey} to decrypt {@code applicationKeys}. 811 * 812 * @return Map from alias to raw key material. 813 * @throws RemoteException if an error occurred decrypting the keys. 814 */ 815 private @NonNull Map<String, byte[]> recoverApplicationKeys( 816 @NonNull byte[] recoveryKey, 817 @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException { 818 HashMap<String, byte[]> keyMaterialByAlias = new HashMap<>(); 819 for (WrappedApplicationKey applicationKey : applicationKeys) { 820 String alias = applicationKey.getAlias(); 821 byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial(); 822 823 try { 824 byte[] keyMaterial = 825 KeySyncUtils.decryptApplicationKey(recoveryKey, encryptedKeyMaterial); 826 keyMaterialByAlias.put(alias, keyMaterial); 827 } catch (NoSuchAlgorithmException e) { 828 Log.wtf(TAG, "Missing SecureBox algorithm. AOSP required to support this.", e); 829 throw new ServiceSpecificException( 830 ERROR_SERVICE_INTERNAL_ERROR, e.getMessage()); 831 } catch (InvalidKeyException e) { 832 Log.e(TAG, "Got InvalidKeyException during decrypting application key with alias: " 833 + alias, e); 834 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED, 835 "Failed to recover key with alias '" + alias + "': " + e.getMessage()); 836 } catch (AEADBadTagException e) { 837 Log.e(TAG, "Got AEADBadTagException during decrypting application key with alias: " 838 + alias, e); 839 // Ignore the exception to continue to recover the other application keys. 840 } 841 } 842 if (!applicationKeys.isEmpty() && keyMaterialByAlias.isEmpty()) { 843 Log.e(TAG, "Failed to recover any of the application keys."); 844 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED, 845 "Failed to recover any of the application keys."); 846 } 847 return keyMaterialByAlias; 848 } 849 850 /** 851 * This function can only be used inside LockSettingsService. 852 * 853 * @param storedHashType from {@code CredentialHash} 854 * @param credential - unencrypted String. Password length should be at most 16 symbols {@code 855 * mPasswordMaxLength} 856 * @param userId for user who just unlocked the device. 857 * @hide 858 */ 859 public void lockScreenSecretAvailable( 860 int storedHashType, @NonNull String credential, int userId) { 861 // So as not to block the critical path unlocking the phone, defer to another thread. 862 try { 863 mExecutorService.execute(KeySyncTask.newInstance( 864 mContext, 865 mDatabase, 866 mSnapshotStorage, 867 mListenersStorage, 868 userId, 869 storedHashType, 870 credential, 871 /*credentialUpdated=*/ false)); 872 } catch (NoSuchAlgorithmException e) { 873 Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e); 874 } catch (KeyStoreException e) { 875 Log.e(TAG, "Key store error encountered during recoverable key sync", e); 876 } catch (InsecureUserException e) { 877 Log.wtf(TAG, "Impossible - insecure user, but user just entered lock screen", e); 878 } 879 } 880 881 /** 882 * This function can only be used inside LockSettingsService. 883 * 884 * @param storedHashType from {@code CredentialHash} 885 * @param credential - unencrypted String 886 * @param userId for the user whose lock screen credentials were changed. 887 * @hide 888 */ 889 public void lockScreenSecretChanged( 890 int storedHashType, 891 @Nullable String credential, 892 int userId) { 893 // So as not to block the critical path unlocking the phone, defer to another thread. 894 try { 895 mExecutorService.execute(KeySyncTask.newInstance( 896 mContext, 897 mDatabase, 898 mSnapshotStorage, 899 mListenersStorage, 900 userId, 901 storedHashType, 902 credential, 903 /*credentialUpdated=*/ true)); 904 } catch (NoSuchAlgorithmException e) { 905 Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e); 906 } catch (KeyStoreException e) { 907 Log.e(TAG, "Key store error encountered during recoverable key sync", e); 908 } catch (InsecureUserException e) { 909 Log.e(TAG, "InsecureUserException during lock screen secret update", e); 910 } 911 } 912 913 private void checkRecoverKeyStorePermission() { 914 mContext.enforceCallingOrSelfPermission( 915 Manifest.permission.RECOVER_KEYSTORE, 916 "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission."); 917 } 918 919 private boolean publicKeysMatch(PublicKey publicKey, byte[] vaultParams) { 920 byte[] encodedPublicKey = SecureBox.encodePublicKey(publicKey); 921 return Arrays.equals(encodedPublicKey, Arrays.copyOf(vaultParams, encodedPublicKey.length)); 922 } 923 } 924