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