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 17package com.android.server.locksettings.recoverablekeystore; 18 19import static android.security.keystore.recovery.KeyChainProtectionParams.TYPE_LOCKSCREEN; 20 21import android.annotation.Nullable; 22import android.content.Context; 23import android.security.Scrypt; 24import android.security.keystore.recovery.KeyChainProtectionParams; 25import android.security.keystore.recovery.KeyChainSnapshot; 26import android.security.keystore.recovery.KeyDerivationParams; 27import android.security.keystore.recovery.WrappedApplicationKey; 28import android.util.Log; 29 30import com.android.internal.annotations.VisibleForTesting; 31import com.android.internal.util.ArrayUtils; 32import com.android.internal.widget.LockPatternUtils; 33import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; 34import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; 35 36import java.io.IOException; 37import java.nio.ByteBuffer; 38import java.nio.ByteOrder; 39import java.nio.charset.StandardCharsets; 40import java.security.GeneralSecurityException; 41import java.security.InvalidAlgorithmParameterException; 42import java.security.InvalidKeyException; 43import java.security.KeyStoreException; 44import java.security.MessageDigest; 45import java.security.NoSuchAlgorithmException; 46import java.security.PublicKey; 47import java.security.SecureRandom; 48import java.security.UnrecoverableKeyException; 49import java.security.cert.CertPath; 50import java.security.cert.CertificateException; 51import java.util.ArrayList; 52import java.util.List; 53import java.util.Map; 54 55import javax.crypto.KeyGenerator; 56import javax.crypto.NoSuchPaddingException; 57import javax.crypto.SecretKey; 58 59/** 60 * Task to sync application keys to a remote vault service. 61 * 62 * @hide 63 */ 64public class KeySyncTask implements Runnable { 65 private static final String TAG = "KeySyncTask"; 66 67 private static final String RECOVERY_KEY_ALGORITHM = "AES"; 68 private static final int RECOVERY_KEY_SIZE_BITS = 256; 69 private static final int SALT_LENGTH_BYTES = 16; 70 private static final int LENGTH_PREFIX_BYTES = Integer.BYTES; 71 private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256"; 72 private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10; 73 74 @VisibleForTesting 75 static final int SCRYPT_PARAM_N = 4096; 76 @VisibleForTesting 77 static final int SCRYPT_PARAM_R = 8; 78 @VisibleForTesting 79 static final int SCRYPT_PARAM_P = 1; 80 @VisibleForTesting 81 static final int SCRYPT_PARAM_OUTLEN_BYTES = 32; 82 83 private final RecoverableKeyStoreDb mRecoverableKeyStoreDb; 84 private final int mUserId; 85 private final int mCredentialType; 86 private final String mCredential; 87 private final boolean mCredentialUpdated; 88 private final PlatformKeyManager mPlatformKeyManager; 89 private final RecoverySnapshotStorage mRecoverySnapshotStorage; 90 private final RecoverySnapshotListenersStorage mSnapshotListenersStorage; 91 private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper; 92 private final Scrypt mScrypt; 93 94 public static KeySyncTask newInstance( 95 Context context, 96 RecoverableKeyStoreDb recoverableKeyStoreDb, 97 RecoverySnapshotStorage snapshotStorage, 98 RecoverySnapshotListenersStorage recoverySnapshotListenersStorage, 99 int userId, 100 int credentialType, 101 String credential, 102 boolean credentialUpdated 103 ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException { 104 return new KeySyncTask( 105 recoverableKeyStoreDb, 106 snapshotStorage, 107 recoverySnapshotListenersStorage, 108 userId, 109 credentialType, 110 credential, 111 credentialUpdated, 112 PlatformKeyManager.getInstance(context, recoverableKeyStoreDb), 113 new TestOnlyInsecureCertificateHelper(), 114 new Scrypt()); 115 } 116 117 /** 118 * A new task. 119 * 120 * @param recoverableKeyStoreDb Database where the keys are stored. 121 * @param userId The uid of the user whose profile has been unlocked. 122 * @param credentialType The type of credential as defined in {@code LockPatternUtils} 123 * @param credential The credential, encoded as a {@link String}. 124 * @param credentialUpdated signals weather credentials were updated. 125 * @param platformKeyManager platform key manager 126 * @param testOnlyInsecureCertificateHelper utility class used for end-to-end tests 127 */ 128 @VisibleForTesting 129 KeySyncTask( 130 RecoverableKeyStoreDb recoverableKeyStoreDb, 131 RecoverySnapshotStorage snapshotStorage, 132 RecoverySnapshotListenersStorage recoverySnapshotListenersStorage, 133 int userId, 134 int credentialType, 135 String credential, 136 boolean credentialUpdated, 137 PlatformKeyManager platformKeyManager, 138 TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper, 139 Scrypt scrypt) { 140 mSnapshotListenersStorage = recoverySnapshotListenersStorage; 141 mRecoverableKeyStoreDb = recoverableKeyStoreDb; 142 mUserId = userId; 143 mCredentialType = credentialType; 144 mCredential = credential; 145 mCredentialUpdated = credentialUpdated; 146 mPlatformKeyManager = platformKeyManager; 147 mRecoverySnapshotStorage = snapshotStorage; 148 mTestOnlyInsecureCertificateHelper = testOnlyInsecureCertificateHelper; 149 mScrypt = scrypt; 150 } 151 152 @Override 153 public void run() { 154 try { 155 // Only one task is active If user unlocks phone many times in a short time interval. 156 synchronized(KeySyncTask.class) { 157 syncKeys(); 158 } 159 } catch (Exception e) { 160 Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e); 161 } 162 } 163 164 private void syncKeys() { 165 if (mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) { 166 // Application keys for the user will not be available for sync. 167 Log.w(TAG, "Credentials are not set for user " + mUserId); 168 int generation = mPlatformKeyManager.getGenerationId(mUserId); 169 mPlatformKeyManager.invalidatePlatformKey(mUserId, generation); 170 return; 171 } 172 if (isCustomLockScreen()) { 173 Log.w(TAG, "Unsupported credential type " + mCredentialType + "for user " + mUserId); 174 mRecoverableKeyStoreDb.invalidateKeysForUserIdOnCustomScreenLock(mUserId); 175 return; 176 } 177 178 List<Integer> recoveryAgents = mRecoverableKeyStoreDb.getRecoveryAgents(mUserId); 179 for (int uid : recoveryAgents) { 180 try { 181 syncKeysForAgent(uid); 182 } catch (IOException e) { 183 Log.e(TAG, "IOException during sync for agent " + uid, e); 184 } 185 } 186 if (recoveryAgents.isEmpty()) { 187 Log.w(TAG, "No recovery agent initialized for user " + mUserId); 188 } 189 } 190 191 private boolean isCustomLockScreen() { 192 return mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE 193 && mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_PATTERN 194 && mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; 195 } 196 197 private void syncKeysForAgent(int recoveryAgentUid) throws IOException { 198 boolean shouldRecreateCurrentVersion = false; 199 if (!shouldCreateSnapshot(recoveryAgentUid)) { 200 shouldRecreateCurrentVersion = 201 (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) 202 && (mRecoverySnapshotStorage.get(recoveryAgentUid) == null); 203 if (shouldRecreateCurrentVersion) { 204 Log.d(TAG, "Recreating most recent snapshot"); 205 } else { 206 Log.d(TAG, "Key sync not needed."); 207 return; 208 } 209 } 210 211 String rootCertAlias = 212 mRecoverableKeyStoreDb.getActiveRootOfTrust(mUserId, recoveryAgentUid); 213 rootCertAlias = mTestOnlyInsecureCertificateHelper 214 .getDefaultCertificateAliasIfEmpty(rootCertAlias); 215 216 PublicKey publicKey; 217 CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId, 218 recoveryAgentUid, rootCertAlias); 219 if (certPath != null) { 220 Log.d(TAG, "Using the public key in stored CertPath for syncing"); 221 publicKey = certPath.getCertificates().get(0).getPublicKey(); 222 } else { 223 Log.d(TAG, "Using the stored raw public key for syncing"); 224 publicKey = mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId, 225 recoveryAgentUid); 226 } 227 if (publicKey == null) { 228 Log.w(TAG, "Not initialized for KeySync: no public key set. Cancelling task."); 229 return; 230 } 231 232 byte[] vaultHandle = mRecoverableKeyStoreDb.getServerParams(mUserId, recoveryAgentUid); 233 if (vaultHandle == null) { 234 Log.w(TAG, "No device ID set for user " + mUserId); 235 return; 236 } 237 238 if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias)) { 239 Log.w(TAG, "Insecure root certificate is used by recovery agent " 240 + recoveryAgentUid); 241 if (mTestOnlyInsecureCertificateHelper.doesCredentialSupportInsecureMode( 242 mCredentialType, mCredential)) { 243 Log.w(TAG, "Whitelisted credential is used to generate snapshot by " 244 + "recovery agent "+ recoveryAgentUid); 245 } else { 246 Log.w(TAG, "Non whitelisted credential is used to generate recovery snapshot by " 247 + recoveryAgentUid + " - ignore attempt."); 248 return; // User secret will not be used. 249 } 250 } 251 252 boolean useScryptToHashCredential = shouldUseScryptToHashCredential(); 253 byte[] salt = generateSalt(); 254 byte[] localLskfHash; 255 if (useScryptToHashCredential) { 256 localLskfHash = hashCredentialsByScrypt(salt, mCredential); 257 } else { 258 localLskfHash = hashCredentialsBySaltedSha256(salt, mCredential); 259 } 260 261 Map<String, SecretKey> rawKeys; 262 try { 263 rawKeys = getKeysToSync(recoveryAgentUid); 264 } catch (GeneralSecurityException e) { 265 Log.e(TAG, "Failed to load recoverable keys for sync", e); 266 return; 267 } catch (InsecureUserException e) { 268 Log.e(TAG, "A screen unlock triggered the key sync flow, so user must have " 269 + "lock screen. This should be impossible.", e); 270 return; 271 } catch (BadPlatformKeyException e) { 272 Log.e(TAG, "Loaded keys for same generation ID as platform key, so " 273 + "BadPlatformKeyException should be impossible.", e); 274 return; 275 } catch (IOException e) { 276 Log.e(TAG, "Local database error.", e); 277 return; 278 } 279 // Only include insecure key material for test 280 if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias)) { 281 rawKeys = mTestOnlyInsecureCertificateHelper.keepOnlyWhitelistedInsecureKeys(rawKeys); 282 } 283 284 SecretKey recoveryKey; 285 try { 286 recoveryKey = generateRecoveryKey(); 287 } catch (NoSuchAlgorithmException e) { 288 Log.wtf("AES should never be unavailable", e); 289 return; 290 } 291 292 Map<String, byte[]> encryptedApplicationKeys; 293 try { 294 encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey( 295 recoveryKey, rawKeys); 296 } catch (InvalidKeyException | NoSuchAlgorithmException e) { 297 Log.wtf(TAG, 298 "Should be impossible: could not encrypt application keys with random key", 299 e); 300 return; 301 } 302 303 Long counterId; 304 // counter id is generated exactly once for each credentials value. 305 if (mCredentialUpdated) { 306 counterId = generateAndStoreCounterId(recoveryAgentUid); 307 } else { 308 counterId = mRecoverableKeyStoreDb.getCounterId(mUserId, recoveryAgentUid); 309 if (counterId == null) { 310 counterId = generateAndStoreCounterId(recoveryAgentUid); 311 } 312 } 313 314 byte[] vaultParams = KeySyncUtils.packVaultParams( 315 publicKey, 316 counterId, 317 TRUSTED_HARDWARE_MAX_ATTEMPTS, 318 vaultHandle); 319 320 byte[] encryptedRecoveryKey; 321 try { 322 encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey( 323 publicKey, 324 localLskfHash, 325 vaultParams, 326 recoveryKey); 327 } catch (NoSuchAlgorithmException e) { 328 Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e); 329 return; 330 } catch (InvalidKeyException e) { 331 Log.e(TAG,"Could not encrypt with recovery key", e); 332 return; 333 } 334 335 KeyDerivationParams keyDerivationParams; 336 if (useScryptToHashCredential) { 337 keyDerivationParams = KeyDerivationParams.createScryptParams( 338 salt, /*memoryDifficulty=*/ SCRYPT_PARAM_N); 339 } else { 340 keyDerivationParams = KeyDerivationParams.createSha256Params(salt); 341 } 342 KeyChainProtectionParams keyChainProtectionParams = new KeyChainProtectionParams.Builder() 343 .setUserSecretType(TYPE_LOCKSCREEN) 344 .setLockScreenUiFormat(getUiFormat(mCredentialType, mCredential)) 345 .setKeyDerivationParams(keyDerivationParams) 346 .setSecret(new byte[0]) 347 .build(); 348 349 ArrayList<KeyChainProtectionParams> metadataList = new ArrayList<>(); 350 metadataList.add(keyChainProtectionParams); 351 352 KeyChainSnapshot.Builder keyChainSnapshotBuilder = new KeyChainSnapshot.Builder() 353 .setSnapshotVersion( 354 getSnapshotVersion(recoveryAgentUid, shouldRecreateCurrentVersion)) 355 .setMaxAttempts(TRUSTED_HARDWARE_MAX_ATTEMPTS) 356 .setCounterId(counterId) 357 .setServerParams(vaultHandle) 358 .setKeyChainProtectionParams(metadataList) 359 .setWrappedApplicationKeys(createApplicationKeyEntries(encryptedApplicationKeys)) 360 .setEncryptedRecoveryKeyBlob(encryptedRecoveryKey); 361 try { 362 keyChainSnapshotBuilder.setTrustedHardwareCertPath(certPath); 363 } catch(CertificateException e) { 364 // Should not happen, as it's just deserialized from bytes stored in the db 365 Log.wtf(TAG, "Cannot serialize CertPath when calling setTrustedHardwareCertPath", e); 366 return; 367 } 368 mRecoverySnapshotStorage.put(recoveryAgentUid, keyChainSnapshotBuilder.build()); 369 mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid); 370 371 mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, false); 372 } 373 374 @VisibleForTesting 375 int getSnapshotVersion(int recoveryAgentUid, boolean shouldRecreateCurrentVersion) 376 throws IOException { 377 Long snapshotVersion = mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid); 378 if (shouldRecreateCurrentVersion) { 379 // version shouldn't be null at this moment. 380 snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion; 381 } else { 382 snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion + 1; 383 } 384 385 long updatedRows = mRecoverableKeyStoreDb.setSnapshotVersion(mUserId, recoveryAgentUid, 386 snapshotVersion); 387 if (updatedRows < 0) { 388 Log.e(TAG, "Failed to set the snapshot version in the local DB."); 389 throw new IOException("Failed to set the snapshot version in the local DB."); 390 } 391 392 return snapshotVersion.intValue(); 393 } 394 395 private long generateAndStoreCounterId(int recoveryAgentUid) throws IOException { 396 long counter = new SecureRandom().nextLong(); 397 long updatedRows = mRecoverableKeyStoreDb.setCounterId(mUserId, recoveryAgentUid, counter); 398 if (updatedRows < 0) { 399 Log.e(TAG, "Failed to set the snapshot version in the local DB."); 400 throw new IOException("Failed to set counterId in the local DB."); 401 } 402 return counter; 403 } 404 405 /** 406 * Returns all of the recoverable keys for the user. 407 */ 408 private Map<String, SecretKey> getKeysToSync(int recoveryAgentUid) 409 throws InsecureUserException, KeyStoreException, UnrecoverableKeyException, 410 NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException, 411 InvalidKeyException, InvalidAlgorithmParameterException, IOException { 412 PlatformDecryptionKey decryptKey = mPlatformKeyManager.getDecryptKey(mUserId);; 413 Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys( 414 mUserId, recoveryAgentUid, decryptKey.getGenerationId()); 415 return WrappedKey.unwrapKeys(decryptKey, wrappedKeys); 416 } 417 418 /** 419 * Returns {@code true} if a sync is pending. 420 * @param recoveryAgentUid uid of the recovery agent. 421 */ 422 private boolean shouldCreateSnapshot(int recoveryAgentUid) { 423 int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid); 424 if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) { 425 // Only lockscreen type is supported. 426 // We will need to pass extra argument to KeySyncTask to support custom pass phrase. 427 return false; 428 } 429 if (mCredentialUpdated) { 430 // Sync credential if at least one snapshot was created. 431 if (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) { 432 mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, true); 433 return true; 434 } 435 } 436 437 return mRecoverableKeyStoreDb.getShouldCreateSnapshot(mUserId, recoveryAgentUid); 438 } 439 440 /** 441 * The UI best suited to entering the given lock screen. This is synced with the vault so the 442 * user can be shown the same UI when recovering the vault on another device. 443 * 444 * @return The format - either pattern, pin, or password. 445 */ 446 @VisibleForTesting 447 @KeyChainProtectionParams.LockScreenUiFormat static int getUiFormat( 448 int credentialType, String credential) { 449 if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) { 450 return KeyChainProtectionParams.UI_FORMAT_PATTERN; 451 } else if (isPin(credential)) { 452 return KeyChainProtectionParams.UI_FORMAT_PIN; 453 } else { 454 return KeyChainProtectionParams.UI_FORMAT_PASSWORD; 455 } 456 } 457 458 /** 459 * Generates a salt to include with the lock screen hash. 460 * 461 * @return The salt. 462 */ 463 private static byte[] generateSalt() { 464 byte[] salt = new byte[SALT_LENGTH_BYTES]; 465 new SecureRandom().nextBytes(salt); 466 return salt; 467 } 468 469 /** 470 * Returns {@code true} if {@code credential} looks like a pin. 471 */ 472 @VisibleForTesting 473 static boolean isPin(@Nullable String credential) { 474 if (credential == null) { 475 return false; 476 } 477 int length = credential.length(); 478 for (int i = 0; i < length; i++) { 479 if (!Character.isDigit(credential.charAt(i))) { 480 return false; 481 } 482 } 483 return true; 484 } 485 486 /** 487 * Hashes {@code credentials} with the given {@code salt}. 488 * 489 * @return The SHA-256 hash. 490 */ 491 @VisibleForTesting 492 static byte[] hashCredentialsBySaltedSha256(byte[] salt, String credentials) { 493 byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8); 494 ByteBuffer byteBuffer = ByteBuffer.allocate( 495 salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2); 496 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 497 byteBuffer.putInt(salt.length); 498 byteBuffer.put(salt); 499 byteBuffer.putInt(credentialsBytes.length); 500 byteBuffer.put(credentialsBytes); 501 byte[] bytes = byteBuffer.array(); 502 503 try { 504 return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes); 505 } catch (NoSuchAlgorithmException e) { 506 // Impossible, SHA-256 must be supported on Android. 507 throw new RuntimeException(e); 508 } 509 } 510 511 private byte[] hashCredentialsByScrypt(byte[] salt, String credentials) { 512 return mScrypt.scrypt( 513 credentials.getBytes(StandardCharsets.UTF_8), salt, 514 SCRYPT_PARAM_N, SCRYPT_PARAM_R, SCRYPT_PARAM_P, SCRYPT_PARAM_OUTLEN_BYTES); 515 } 516 517 private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException { 518 KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM); 519 keyGenerator.init(RECOVERY_KEY_SIZE_BITS); 520 return keyGenerator.generateKey(); 521 } 522 523 private static List<WrappedApplicationKey> createApplicationKeyEntries( 524 Map<String, byte[]> encryptedApplicationKeys) { 525 ArrayList<WrappedApplicationKey> keyEntries = new ArrayList<>(); 526 for (String alias : encryptedApplicationKeys.keySet()) { 527 keyEntries.add(new WrappedApplicationKey.Builder() 528 .setAlias(alias) 529 .setEncryptedKeyMaterial(encryptedApplicationKeys.get(alias)) 530 .build()); 531 } 532 return keyEntries; 533 } 534 535 private boolean shouldUseScryptToHashCredential() { 536 return mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; 537 } 538} 539