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