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