KeySyncTask.java revision c3aefbd7f8def01618f399f427ddf7c11aeaa88c
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.Scrypt;
24import android.security.keystore.recovery.KeyChainProtectionParams;
25import android.security.keystore.recovery.KeyChainSnapshot;
26import android.security.keystore.recovery.KeyDerivationParams;
27import android.security.keystore.recovery.WrappedApplicationKey;
28import android.util.Log;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.internal.util.ArrayUtils;
32import com.android.internal.widget.LockPatternUtils;
33import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
34import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
35
36import java.nio.ByteBuffer;
37import java.nio.ByteOrder;
38import java.nio.charset.StandardCharsets;
39import java.security.GeneralSecurityException;
40import java.security.InvalidAlgorithmParameterException;
41import java.security.InvalidKeyException;
42import java.security.KeyStoreException;
43import java.security.MessageDigest;
44import java.security.NoSuchAlgorithmException;
45import java.security.PublicKey;
46import java.security.SecureRandom;
47import java.security.UnrecoverableKeyException;
48import java.security.cert.CertPath;
49import java.security.cert.CertificateException;
50import java.util.ArrayList;
51import java.util.List;
52import java.util.Map;
53
54import javax.crypto.KeyGenerator;
55import javax.crypto.NoSuchPaddingException;
56import javax.crypto.SecretKey;
57
58/**
59 * Task to sync application keys to a remote vault service.
60 *
61 * @hide
62 */
63public class KeySyncTask implements Runnable {
64    private static final String TAG = "KeySyncTask";
65
66    private static final String RECOVERY_KEY_ALGORITHM = "AES";
67    private static final int RECOVERY_KEY_SIZE_BITS = 256;
68    private static final int SALT_LENGTH_BYTES = 16;
69    private static final int LENGTH_PREFIX_BYTES = Integer.BYTES;
70    private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
71    private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10;
72
73    @VisibleForTesting
74    static final int SCRYPT_PARAM_N = 4096;
75    @VisibleForTesting
76    static final int SCRYPT_PARAM_R = 8;
77    @VisibleForTesting
78    static final int SCRYPT_PARAM_P = 1;
79    @VisibleForTesting
80    static final int SCRYPT_PARAM_OUTLEN_BYTES = 32;
81
82    private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
83    private final int mUserId;
84    private final int mCredentialType;
85    private final String mCredential;
86    private final boolean mCredentialUpdated;
87    private final PlatformKeyManager mPlatformKeyManager;
88    private final RecoverySnapshotStorage mRecoverySnapshotStorage;
89    private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
90    private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
91    private final Scrypt mScrypt;
92
93    public static KeySyncTask newInstance(
94            Context context,
95            RecoverableKeyStoreDb recoverableKeyStoreDb,
96            RecoverySnapshotStorage snapshotStorage,
97            RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
98            int userId,
99            int credentialType,
100            String credential,
101            boolean credentialUpdated
102    ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
103        return new KeySyncTask(
104                recoverableKeyStoreDb,
105                snapshotStorage,
106                recoverySnapshotListenersStorage,
107                userId,
108                credentialType,
109                credential,
110                credentialUpdated,
111                PlatformKeyManager.getInstance(context, recoverableKeyStoreDb),
112                new TestOnlyInsecureCertificateHelper(),
113                new Scrypt());
114    }
115
116    /**
117     * A new task.
118     *
119     * @param recoverableKeyStoreDb Database where the keys are stored.
120     * @param userId The uid of the user whose profile has been unlocked.
121     * @param credentialType The type of credential as defined in {@code LockPatternUtils}
122     * @param credential The credential, encoded as a {@link String}.
123     * @param credentialUpdated signals weather credentials were updated.
124     * @param platformKeyManager platform key manager
125     * @param testOnlyInsecureCertificateHelper utility class used for end-to-end tests
126     */
127    @VisibleForTesting
128    KeySyncTask(
129            RecoverableKeyStoreDb recoverableKeyStoreDb,
130            RecoverySnapshotStorage snapshotStorage,
131            RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
132            int userId,
133            int credentialType,
134            String credential,
135            boolean credentialUpdated,
136            PlatformKeyManager platformKeyManager,
137            TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
138            Scrypt scrypt) {
139        mSnapshotListenersStorage = recoverySnapshotListenersStorage;
140        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
141        mUserId = userId;
142        mCredentialType = credentialType;
143        mCredential = credential;
144        mCredentialUpdated = credentialUpdated;
145        mPlatformKeyManager = platformKeyManager;
146        mRecoverySnapshotStorage = snapshotStorage;
147        mTestOnlyInsecureCertificateHelper = testOnlyInsecureCertificateHelper;
148        mScrypt = scrypt;
149    }
150
151    @Override
152    public void run() {
153        try {
154            // Only one task is active If user unlocks phone many times in a short time interval.
155            synchronized(KeySyncTask.class) {
156                syncKeys();
157            }
158        } catch (Exception e) {
159            Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
160        }
161    }
162
163    private void syncKeys() {
164        if (mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
165            // Application keys for the user will not be available for sync.
166            Log.w(TAG, "Credentials are not set for user " + mUserId);
167            int generation = mPlatformKeyManager.getGenerationId(mUserId);
168            mPlatformKeyManager.invalidatePlatformKey(mUserId, generation);
169            return;
170        }
171        if (isCustomLockScreen()) {
172            Log.w(TAG, "Unsupported credential type " + mCredentialType + "for user " + mUserId);
173            mRecoverableKeyStoreDb.invalidateKeysForUserIdOnCustomScreenLock(mUserId);
174            return;
175        }
176
177        List<Integer> recoveryAgents = mRecoverableKeyStoreDb.getRecoveryAgents(mUserId);
178        for (int uid : recoveryAgents) {
179            syncKeysForAgent(uid);
180        }
181        if (recoveryAgents.isEmpty()) {
182            Log.w(TAG, "No recovery agent initialized for user " + mUserId);
183        }
184    }
185
186    private boolean isCustomLockScreen() {
187        return mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE
188            && mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_PATTERN
189            && mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
190    }
191
192    private void syncKeysForAgent(int recoveryAgentUid) {
193        boolean recreateCurrentVersion = false;
194        if (!shouldCreateSnapshot(recoveryAgentUid)) {
195            recreateCurrentVersion =
196                    (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null)
197                    && (mRecoverySnapshotStorage.get(recoveryAgentUid) == null);
198            if (recreateCurrentVersion) {
199                Log.d(TAG, "Recreating most recent snapshot");
200            } else {
201                Log.d(TAG, "Key sync not needed.");
202                return;
203            }
204        }
205
206        PublicKey publicKey;
207        String rootCertAlias =
208                mRecoverableKeyStoreDb.getActiveRootOfTrust(mUserId, recoveryAgentUid);
209        rootCertAlias = mTestOnlyInsecureCertificateHelper
210                .getDefaultCertificateAliasIfEmpty(rootCertAlias);
211
212        CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId,
213                recoveryAgentUid, rootCertAlias);
214        if (certPath != null) {
215            Log.d(TAG, "Using the public key in stored CertPath for syncing");
216            publicKey = certPath.getCertificates().get(0).getPublicKey();
217        } else {
218            Log.d(TAG, "Using the stored raw public key for syncing");
219            publicKey = mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId,
220                    recoveryAgentUid);
221        }
222        if (publicKey == null) {
223            Log.w(TAG, "Not initialized for KeySync: no public key set. Cancelling task.");
224            return;
225        }
226
227        byte[] vaultHandle = mRecoverableKeyStoreDb.getServerParams(mUserId, recoveryAgentUid);
228        if (vaultHandle == null) {
229            Log.w(TAG, "No device ID set for user " + mUserId);
230            return;
231        }
232
233        if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias)) {
234            Log.w(TAG, "Insecure root certificate is used by recovery agent "
235                    + recoveryAgentUid);
236            if (mTestOnlyInsecureCertificateHelper.doesCredentialSupportInsecureMode(
237                    mCredentialType, mCredential)) {
238                Log.w(TAG, "Whitelisted credential is used to generate snapshot by "
239                        + "recovery agent "+ recoveryAgentUid);
240            } else {
241                Log.w(TAG, "Non whitelisted credential is used to generate recovery snapshot by "
242                        + recoveryAgentUid + " - ignore attempt.");
243                return; // User secret will not be used.
244            }
245        }
246
247        boolean useScryptToHashCredential = shouldUseScryptToHashCredential();
248        byte[] salt = generateSalt();
249        byte[] localLskfHash;
250        if (useScryptToHashCredential) {
251            localLskfHash = hashCredentialsByScrypt(salt, mCredential);
252        } else {
253            localLskfHash = hashCredentialsBySaltedSha256(salt, mCredential);
254        }
255
256        Map<String, SecretKey> rawKeys;
257        try {
258            rawKeys = getKeysToSync(recoveryAgentUid);
259        } catch (GeneralSecurityException e) {
260            Log.e(TAG, "Failed to load recoverable keys for sync", e);
261            return;
262        } catch (InsecureUserException e) {
263            Log.wtf(TAG, "A screen unlock triggered the key sync flow, so user must have "
264                    + "lock screen. This should be impossible.", e);
265            return;
266        } catch (BadPlatformKeyException e) {
267            Log.wtf(TAG, "Loaded keys for same generation ID as platform key, so "
268                    + "BadPlatformKeyException should be impossible.", e);
269            return;
270        }
271
272        // Only include insecure key material for test
273        if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias)) {
274            rawKeys = mTestOnlyInsecureCertificateHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
275        }
276        SecretKey recoveryKey;
277        try {
278            recoveryKey = generateRecoveryKey();
279        } catch (NoSuchAlgorithmException e) {
280            Log.wtf("AES should never be unavailable", e);
281            return;
282        }
283
284        Map<String, byte[]> encryptedApplicationKeys;
285        try {
286            encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey(
287                    recoveryKey, rawKeys);
288        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
289            Log.wtf(TAG,
290                    "Should be impossible: could not encrypt application keys with random key",
291                    e);
292            return;
293        }
294
295        Long counterId;
296        // counter id is generated exactly once for each credentials value.
297        if (mCredentialUpdated) {
298            counterId = generateAndStoreCounterId(recoveryAgentUid);
299        } else {
300            counterId = mRecoverableKeyStoreDb.getCounterId(mUserId, recoveryAgentUid);
301            if (counterId == null) {
302                counterId = generateAndStoreCounterId(recoveryAgentUid);
303            }
304        }
305
306        byte[] vaultParams = KeySyncUtils.packVaultParams(
307                publicKey,
308                counterId,
309                TRUSTED_HARDWARE_MAX_ATTEMPTS,
310                vaultHandle);
311
312        byte[] encryptedRecoveryKey;
313        try {
314            encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
315                    publicKey,
316                    localLskfHash,
317                    vaultParams,
318                    recoveryKey);
319        } catch (NoSuchAlgorithmException e) {
320            Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
321            return;
322        } catch (InvalidKeyException e) {
323            Log.e(TAG,"Could not encrypt with recovery key", e);
324            return;
325        }
326        KeyDerivationParams keyDerivationParams;
327        if (useScryptToHashCredential) {
328            keyDerivationParams = KeyDerivationParams.createScryptParams(
329                    salt, /*memoryDifficulty=*/ SCRYPT_PARAM_N);
330        } else {
331            keyDerivationParams = KeyDerivationParams.createSha256Params(salt);
332        }
333        KeyChainProtectionParams metadata = new KeyChainProtectionParams.Builder()
334                .setUserSecretType(TYPE_LOCKSCREEN)
335                .setLockScreenUiFormat(getUiFormat(mCredentialType, mCredential))
336                .setKeyDerivationParams(keyDerivationParams)
337                .setSecret(new byte[0])
338                .build();
339
340        ArrayList<KeyChainProtectionParams> metadataList = new ArrayList<>();
341        metadataList.add(metadata);
342
343        // If application keys are not updated, snapshot will not be created on next unlock.
344        mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, false);
345
346        KeyChainSnapshot.Builder keyChainSnapshotBuilder = new KeyChainSnapshot.Builder()
347                .setSnapshotVersion(getSnapshotVersion(recoveryAgentUid, recreateCurrentVersion))
348                .setMaxAttempts(TRUSTED_HARDWARE_MAX_ATTEMPTS)
349                .setCounterId(counterId)
350                .setServerParams(vaultHandle)
351                .setKeyChainProtectionParams(metadataList)
352                .setWrappedApplicationKeys(createApplicationKeyEntries(encryptedApplicationKeys))
353                .setEncryptedRecoveryKeyBlob(encryptedRecoveryKey);
354        try {
355            keyChainSnapshotBuilder.setTrustedHardwareCertPath(certPath);
356        } catch(CertificateException e) {
357            // Should not happen, as it's just deserialized from bytes stored in the db
358            Log.wtf(TAG, "Cannot serialize CertPath when calling setTrustedHardwareCertPath", e);
359            return;
360        }
361        mRecoverySnapshotStorage.put(recoveryAgentUid, keyChainSnapshotBuilder.build());
362        mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
363    }
364
365    @VisibleForTesting
366    int getSnapshotVersion(int recoveryAgentUid, boolean recreateCurrentVersion) {
367        Long snapshotVersion = mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid);
368        if (recreateCurrentVersion) {
369            // version shouldn't be null at this moment.
370            snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion;
371        } else {
372            snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion + 1;
373        }
374        mRecoverableKeyStoreDb.setSnapshotVersion(mUserId, recoveryAgentUid, snapshotVersion);
375
376        return snapshotVersion.intValue();
377    }
378
379    private long generateAndStoreCounterId(int recoveryAgentUid) {
380        long counter = new SecureRandom().nextLong();
381        mRecoverableKeyStoreDb.setCounterId(mUserId, recoveryAgentUid, counter);
382        return counter;
383    }
384
385    /**
386     * Returns all of the recoverable keys for the user.
387     */
388    private Map<String, SecretKey> getKeysToSync(int recoveryAgentUid)
389            throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
390            NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
391            InvalidKeyException, InvalidAlgorithmParameterException {
392        PlatformDecryptionKey decryptKey = mPlatformKeyManager.getDecryptKey(mUserId);;
393        Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
394                mUserId, recoveryAgentUid, decryptKey.getGenerationId());
395        return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
396    }
397
398    /**
399     * Returns {@code true} if a sync is pending.
400     * @param recoveryAgentUid uid of the recovery agent.
401     */
402    private boolean shouldCreateSnapshot(int recoveryAgentUid) {
403        int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid);
404        if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) {
405            // Only lockscreen type is supported.
406            // We will need to pass extra argument to KeySyncTask to support custom pass phrase.
407            return false;
408        }
409        if (mCredentialUpdated) {
410            // Sync credential if at least one snapshot was created.
411            if (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) {
412                mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, true);
413                return true;
414            }
415        }
416
417        return mRecoverableKeyStoreDb.getShouldCreateSnapshot(mUserId, recoveryAgentUid);
418    }
419
420    /**
421     * The UI best suited to entering the given lock screen. This is synced with the vault so the
422     * user can be shown the same UI when recovering the vault on another device.
423     *
424     * @return The format - either pattern, pin, or password.
425     */
426    @VisibleForTesting
427    @KeyChainProtectionParams.LockScreenUiFormat static int getUiFormat(
428            int credentialType, String credential) {
429        if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
430            return KeyChainProtectionParams.UI_FORMAT_PATTERN;
431        } else if (isPin(credential)) {
432            return KeyChainProtectionParams.UI_FORMAT_PIN;
433        } else {
434            return KeyChainProtectionParams.UI_FORMAT_PASSWORD;
435        }
436    }
437
438    /**
439     * Generates a salt to include with the lock screen hash.
440     *
441     * @return The salt.
442     */
443    private byte[] generateSalt() {
444        byte[] salt = new byte[SALT_LENGTH_BYTES];
445        new SecureRandom().nextBytes(salt);
446        return salt;
447    }
448
449    /**
450     * Returns {@code true} if {@code credential} looks like a pin.
451     */
452    @VisibleForTesting
453    static boolean isPin(@Nullable String credential) {
454        if (credential == null) {
455            return false;
456        }
457        int length = credential.length();
458        for (int i = 0; i < length; i++) {
459            if (!Character.isDigit(credential.charAt(i))) {
460                return false;
461            }
462        }
463        return true;
464    }
465
466    /**
467     * Hashes {@code credentials} with the given {@code salt}.
468     *
469     * @return The SHA-256 hash.
470     */
471    @VisibleForTesting
472    static byte[] hashCredentialsBySaltedSha256(byte[] salt, String credentials) {
473        byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
474        ByteBuffer byteBuffer = ByteBuffer.allocate(
475                salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
476        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
477        byteBuffer.putInt(salt.length);
478        byteBuffer.put(salt);
479        byteBuffer.putInt(credentialsBytes.length);
480        byteBuffer.put(credentialsBytes);
481        byte[] bytes = byteBuffer.array();
482
483        try {
484            return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
485        } catch (NoSuchAlgorithmException e) {
486            // Impossible, SHA-256 must be supported on Android.
487            throw new RuntimeException(e);
488        }
489    }
490
491    private byte[] hashCredentialsByScrypt(byte[] salt, String credentials) {
492        return mScrypt.scrypt(
493                credentials.getBytes(StandardCharsets.UTF_8), salt,
494                SCRYPT_PARAM_N, SCRYPT_PARAM_R, SCRYPT_PARAM_P, SCRYPT_PARAM_OUTLEN_BYTES);
495    }
496
497    private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
498        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
499        keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
500        return keyGenerator.generateKey();
501    }
502
503    private static List<WrappedApplicationKey> createApplicationKeyEntries(
504            Map<String, byte[]> encryptedApplicationKeys) {
505        ArrayList<WrappedApplicationKey> keyEntries = new ArrayList<>();
506        for (String alias : encryptedApplicationKeys.keySet()) {
507            keyEntries.add(new WrappedApplicationKey.Builder()
508                    .setAlias(alias)
509                    .setEncryptedKeyMaterial(encryptedApplicationKeys.get(alias))
510                    .build());
511        }
512        return keyEntries;
513    }
514
515    private boolean shouldUseScryptToHashCredential() {
516        return mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
517    }
518}
519