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