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