KeySyncTask.java revision 2fd4b597ae3cfaa5dfa8156ec15bc813d69acf7a
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        // TODO: make sure the same counter id is used during recovery and remove temporary fix.
259        counterId = 1L;
260
261        byte[] vaultParams = KeySyncUtils.packVaultParams(
262                publicKey,
263                counterId,
264                TRUSTED_HARDWARE_MAX_ATTEMPTS,
265                vaultHandle);
266
267        byte[] encryptedRecoveryKey;
268        try {
269            encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
270                    publicKey,
271                    localLskfHash,
272                    vaultParams,
273                    recoveryKey);
274        } catch (NoSuchAlgorithmException e) {
275            Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
276            return;
277        } catch (InvalidKeyException e) {
278            Log.e(TAG,"Could not encrypt with recovery key", e);
279            return;
280        }
281        KeyChainProtectionParams metadata = new KeyChainProtectionParams.Builder()
282                .setUserSecretType(TYPE_LOCKSCREEN)
283                .setLockScreenUiFormat(getUiFormat(mCredentialType, mCredential))
284                .setKeyDerivationParams(KeyDerivationParams.createSha256Params(salt))
285                .setSecret(new byte[0])
286                .build();
287
288        ArrayList<KeyChainProtectionParams> metadataList = new ArrayList<>();
289        metadataList.add(metadata);
290
291        // If application keys are not updated, snapshot will not be created on next unlock.
292        mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, false);
293
294        KeyChainSnapshot.Builder keyChainSnapshotBuilder = new KeyChainSnapshot.Builder()
295                .setSnapshotVersion(getSnapshotVersion(recoveryAgentUid, recreateCurrentVersion))
296                .setMaxAttempts(TRUSTED_HARDWARE_MAX_ATTEMPTS)
297                .setCounterId(counterId)
298                .setTrustedHardwarePublicKey(SecureBox.encodePublicKey(publicKey))
299                .setServerParams(vaultHandle)
300                .setKeyChainProtectionParams(metadataList)
301                .setWrappedApplicationKeys(createApplicationKeyEntries(encryptedApplicationKeys))
302                .setEncryptedRecoveryKeyBlob(encryptedRecoveryKey);
303        try {
304            keyChainSnapshotBuilder.setTrustedHardwareCertPath(certPath);
305        } catch(CertificateException e) {
306            // Should not happen, as it's just deserialized from bytes stored in the db
307            Log.wtf(TAG, "Cannot serialize CertPath when calling setTrustedHardwareCertPath", e);
308            return;
309        }
310        mRecoverySnapshotStorage.put(recoveryAgentUid, keyChainSnapshotBuilder.build());
311        mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
312    }
313
314    @VisibleForTesting
315    int getSnapshotVersion(int recoveryAgentUid, boolean recreateCurrentVersion) {
316        Long snapshotVersion = mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid);
317        if (recreateCurrentVersion) {
318            // version shouldn't be null at this moment.
319            snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion;
320        } else {
321            snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion + 1;
322        }
323        mRecoverableKeyStoreDb.setSnapshotVersion(mUserId, recoveryAgentUid, snapshotVersion);
324
325        return snapshotVersion.intValue();
326    }
327
328    private long generateAndStoreCounterId(int recoveryAgentUid) {
329        long counter = new SecureRandom().nextLong();
330        mRecoverableKeyStoreDb.setCounterId(mUserId, recoveryAgentUid, counter);
331        return counter;
332    }
333
334    /**
335     * Returns all of the recoverable keys for the user.
336     */
337    private Map<String, SecretKey> getKeysToSync(int recoveryAgentUid)
338            throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
339            NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
340            InvalidKeyException, InvalidAlgorithmParameterException {
341        PlatformDecryptionKey decryptKey = mPlatformKeyManager.getDecryptKey(mUserId);;
342        Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
343                mUserId, recoveryAgentUid, decryptKey.getGenerationId());
344        return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
345    }
346
347    /**
348     * Returns {@code true} if a sync is pending.
349     * @param recoveryAgentUid uid of the recovery agent.
350     */
351    private boolean shouldCreateSnapshot(int recoveryAgentUid) {
352        int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid);
353        if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) {
354            // Only lockscreen type is supported.
355            // We will need to pass extra argument to KeySyncTask to support custom pass phrase.
356            return false;
357        }
358        if (mCredentialUpdated) {
359            // Sync credential if at least one snapshot was created.
360            if (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) {
361                mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, true);
362                return true;
363            }
364        }
365
366        return mRecoverableKeyStoreDb.getShouldCreateSnapshot(mUserId, recoveryAgentUid);
367    }
368
369    /**
370     * The UI best suited to entering the given lock screen. This is synced with the vault so the
371     * user can be shown the same UI when recovering the vault on another device.
372     *
373     * @return The format - either pattern, pin, or password.
374     */
375    @VisibleForTesting
376    @KeyChainProtectionParams.LockScreenUiFormat static int getUiFormat(
377            int credentialType, String credential) {
378        if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
379            return KeyChainProtectionParams.UI_FORMAT_PATTERN;
380        } else if (isPin(credential)) {
381            return KeyChainProtectionParams.UI_FORMAT_PIN;
382        } else {
383            return KeyChainProtectionParams.UI_FORMAT_PASSWORD;
384        }
385    }
386
387    /**
388     * Generates a salt to include with the lock screen hash.
389     *
390     * @return The salt.
391     */
392    private byte[] generateSalt() {
393        byte[] salt = new byte[SALT_LENGTH_BYTES];
394        new SecureRandom().nextBytes(salt);
395        return salt;
396    }
397
398    /**
399     * Returns {@code true} if {@code credential} looks like a pin.
400     */
401    @VisibleForTesting
402    static boolean isPin(@Nullable String credential) {
403        if (credential == null) {
404            return false;
405        }
406        int length = credential.length();
407        for (int i = 0; i < length; i++) {
408            if (!Character.isDigit(credential.charAt(i))) {
409                return false;
410            }
411        }
412        return true;
413    }
414
415    /**
416     * Hashes {@code credentials} with the given {@code salt}.
417     *
418     * @return The SHA-256 hash.
419     */
420    @VisibleForTesting
421    static byte[] hashCredentials(byte[] salt, String credentials) {
422        byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
423        ByteBuffer byteBuffer = ByteBuffer.allocate(
424                salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
425        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
426        byteBuffer.putInt(salt.length);
427        byteBuffer.put(salt);
428        byteBuffer.putInt(credentialsBytes.length);
429        byteBuffer.put(credentialsBytes);
430        byte[] bytes = byteBuffer.array();
431
432        try {
433            return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
434        } catch (NoSuchAlgorithmException e) {
435            // Impossible, SHA-256 must be supported on Android.
436            throw new RuntimeException(e);
437        }
438    }
439
440    private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
441        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
442        keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
443        return keyGenerator.generateKey();
444    }
445
446    private static List<WrappedApplicationKey> createApplicationKeyEntries(
447            Map<String, byte[]> encryptedApplicationKeys) {
448        ArrayList<WrappedApplicationKey> keyEntries = new ArrayList<>();
449        for (String alias : encryptedApplicationKeys.keySet()) {
450            keyEntries.add(new WrappedApplicationKey.Builder()
451                    .setAlias(alias)
452                    .setEncryptedKeyMaterial(encryptedApplicationKeys.get(alias))
453                    .build());
454        }
455        return keyEntries;
456    }
457}
458