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