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