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