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