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