KeySyncTask.java revision bd086f1963f13d13a03928f41b9b7979bebffa26
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 67 private final RecoverableKeyStoreDb mRecoverableKeyStoreDb; 68 private final int mUserId; 69 private final int mCredentialType; 70 private final String mCredential; 71 private final PlatformKeyManager.Factory mPlatformKeyManagerFactory; 72 private final VaultKeySupplier mVaultKeySupplier; 73 private final RecoverySnapshotStorage mRecoverySnapshotStorage; 74 75 public static KeySyncTask newInstance( 76 Context context, 77 RecoverableKeyStoreDb recoverableKeyStoreDb, 78 RecoverySnapshotStorage snapshotStorage, 79 int userId, 80 int credentialType, 81 String credential 82 ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException { 83 return new KeySyncTask( 84 recoverableKeyStoreDb, 85 snapshotStorage, 86 userId, 87 credentialType, 88 credential, 89 () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId), 90 () -> { 91 throw new UnsupportedOperationException("Not implemented vault key."); 92 }); 93 } 94 95 /** 96 * A new task. 97 * 98 * @param recoverableKeyStoreDb Database where the keys are stored. 99 * @param userId The uid of the user whose profile has been unlocked. 100 * @param credentialType The type of credential - i.e., pattern or password. 101 * @param credential The credential, encoded as a {@link String}. 102 * @param platformKeyManagerFactory Instantiates a {@link PlatformKeyManager} for the user. 103 * This is a factory to enable unit testing, as otherwise it would be impossible to test 104 * without a screen unlock occurring! 105 */ 106 @VisibleForTesting 107 KeySyncTask( 108 RecoverableKeyStoreDb recoverableKeyStoreDb, 109 RecoverySnapshotStorage snapshotStorage, 110 int userId, 111 int credentialType, 112 String credential, 113 PlatformKeyManager.Factory platformKeyManagerFactory, 114 VaultKeySupplier vaultKeySupplier) { 115 mRecoverableKeyStoreDb = recoverableKeyStoreDb; 116 mUserId = userId; 117 mCredentialType = credentialType; 118 mCredential = credential; 119 mPlatformKeyManagerFactory = platformKeyManagerFactory; 120 mVaultKeySupplier = vaultKeySupplier; 121 mRecoverySnapshotStorage = snapshotStorage; 122 } 123 124 @Override 125 public void run() { 126 try { 127 syncKeys(); 128 } catch (Exception e) { 129 Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e); 130 } 131 } 132 133 private void syncKeys() { 134 if (!isSyncPending()) { 135 Log.d(TAG, "Key sync not needed."); 136 return; 137 } 138 139 byte[] salt = generateSalt(); 140 byte[] localLskfHash = hashCredentials(salt, mCredential); 141 142 Map<String, SecretKey> rawKeys; 143 try { 144 rawKeys = getKeysToSync(); 145 } catch (GeneralSecurityException e) { 146 Log.e(TAG, "Failed to load recoverable keys for sync", e); 147 return; 148 } catch (InsecureUserException e) { 149 Log.wtf(TAG, "A screen unlock triggered the key sync flow, so user must have " 150 + "lock screen. This should be impossible.", e); 151 return; 152 } catch (BadPlatformKeyException e) { 153 Log.wtf(TAG, "Loaded keys for same generation ID as platform key, so " 154 + "BadPlatformKeyException should be impossible.", e); 155 return; 156 } 157 158 SecretKey recoveryKey; 159 try { 160 recoveryKey = generateRecoveryKey(); 161 } catch (NoSuchAlgorithmException e) { 162 Log.wtf("AES should never be unavailable", e); 163 return; 164 } 165 166 Map<String, byte[]> encryptedApplicationKeys; 167 try { 168 encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey( 169 recoveryKey, rawKeys); 170 } catch (InvalidKeyException | NoSuchAlgorithmException e) { 171 Log.wtf(TAG, 172 "Should be impossible: could not encrypt application keys with random key", 173 e); 174 return; 175 } 176 177 // TODO: construct vault params and vault metadata 178 byte[] vaultParams = {}; 179 180 byte[] encryptedRecoveryKey; 181 try { 182 encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey( 183 mVaultKeySupplier.get(), 184 localLskfHash, 185 vaultParams, 186 recoveryKey); 187 } catch (NoSuchAlgorithmException e) { 188 Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e); 189 return; 190 } catch (InvalidKeyException e) { 191 Log.e(TAG,"Could not encrypt with recovery key", e); 192 return; 193 } 194 195 // TODO: why is the secret sent here? I thought it wasn't sent in the raw at all. 196 KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata( 197 /*userSecretType=*/ TYPE_LOCKSCREEN, 198 /*lockScreenUiFormat=*/ mCredentialType, 199 /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt), 200 /*secret=*/ new byte[0]); 201 ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>(); 202 metadataList.add(metadata); 203 204 // TODO: implement snapshot version 205 mRecoverySnapshotStorage.put(mUserId, new KeyStoreRecoveryData( 206 /*snapshotVersion=*/ 1, 207 /*recoveryMetadata=*/ metadataList, 208 /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys), 209 /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey)); 210 } 211 212 private PublicKey getVaultPublicKey() { 213 // TODO: fill this in 214 throw new UnsupportedOperationException("TODO: get vault public key."); 215 } 216 217 /** 218 * Returns all of the recoverable keys for the user. 219 */ 220 private Map<String, SecretKey> getKeysToSync() 221 throws InsecureUserException, KeyStoreException, UnrecoverableKeyException, 222 NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException { 223 PlatformKeyManager platformKeyManager = mPlatformKeyManagerFactory.newInstance(); 224 PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey(); 225 Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys( 226 mUserId, decryptKey.getGenerationId()); 227 return WrappedKey.unwrapKeys(decryptKey, wrappedKeys); 228 } 229 230 /** 231 * Returns {@code true} if a sync is pending. 232 */ 233 private boolean isSyncPending() { 234 // TODO: implement properly. For now just always syncing if the user has any recoverable 235 // keys. We need to keep track of when the store's state actually changes. 236 return !mRecoverableKeyStoreDb.getAllKeys( 237 mUserId, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(mUserId)).isEmpty(); 238 } 239 240 /** 241 * The UI best suited to entering the given lock screen. This is synced with the vault so the 242 * user can be shown the same UI when recovering the vault on another device. 243 * 244 * @return The format - either pattern, pin, or password. 245 */ 246 @VisibleForTesting 247 @KeyStoreRecoveryMetadata.LockScreenUiFormat static int getUiFormat( 248 int credentialType, String credential) { 249 if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) { 250 return KeyStoreRecoveryMetadata.TYPE_PATTERN; 251 } else if (isPin(credential)) { 252 return KeyStoreRecoveryMetadata.TYPE_PIN; 253 } else { 254 return KeyStoreRecoveryMetadata.TYPE_PASSWORD; 255 } 256 } 257 258 /** 259 * Generates a salt to include with the lock screen hash. 260 * 261 * @return The salt. 262 */ 263 private byte[] generateSalt() { 264 byte[] salt = new byte[SALT_LENGTH_BYTES]; 265 new SecureRandom().nextBytes(salt); 266 return salt; 267 } 268 269 /** 270 * Returns {@code true} if {@code credential} looks like a pin. 271 */ 272 @VisibleForTesting 273 static boolean isPin(@NonNull String credential) { 274 int length = credential.length(); 275 for (int i = 0; i < length; i++) { 276 if (!Character.isDigit(credential.charAt(i))) { 277 return false; 278 } 279 } 280 return true; 281 } 282 283 /** 284 * Hashes {@code credentials} with the given {@code salt}. 285 * 286 * @return The SHA-256 hash. 287 */ 288 @VisibleForTesting 289 static byte[] hashCredentials(byte[] salt, String credentials) { 290 byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8); 291 ByteBuffer byteBuffer = ByteBuffer.allocate( 292 salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2); 293 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); 294 byteBuffer.putInt(salt.length); 295 byteBuffer.put(salt); 296 byteBuffer.putInt(credentialsBytes.length); 297 byteBuffer.put(credentialsBytes); 298 byte[] bytes = byteBuffer.array(); 299 300 try { 301 return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes); 302 } catch (NoSuchAlgorithmException e) { 303 // Impossible, SHA-256 must be supported on Android. 304 throw new RuntimeException(e); 305 } 306 } 307 308 private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException { 309 KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM); 310 keyGenerator.init(RECOVERY_KEY_SIZE_BITS); 311 return keyGenerator.generateKey(); 312 } 313 314 private static List<KeyEntryRecoveryData> createApplicationKeyEntries( 315 Map<String, byte[]> encryptedApplicationKeys) { 316 ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>(); 317 for (String alias : encryptedApplicationKeys.keySet()) { 318 keyEntries.add( 319 new KeyEntryRecoveryData( 320 alias.getBytes(StandardCharsets.UTF_8), 321 encryptedApplicationKeys.get(alias))); 322 } 323 return keyEntries; 324 } 325 326 /** 327 * TODO: until this is in the database, so we can test. 328 */ 329 public interface VaultKeySupplier { 330 PublicKey get(); 331 } 332} 333