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