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