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