KeySyncTask.java revision 77183effbf21cbaa9dd81b31ba5c0e1a580619a3
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.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN;
20
21import android.annotation.NonNull;
22import android.content.Context;
23import android.security.recoverablekeystore.KeyDerivationParameters;
24import android.security.recoverablekeystore.KeyEntryRecoveryData;
25import android.security.recoverablekeystore.KeyStoreRecoveryData;
26import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
27import android.util.Log;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.widget.LockPatternUtils;
31import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
32import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
33
34import java.nio.ByteBuffer;
35import java.nio.ByteOrder;
36import java.nio.charset.StandardCharsets;
37import java.security.GeneralSecurityException;
38import java.security.InvalidKeyException;
39import java.security.KeyStoreException;
40import java.security.MessageDigest;
41import java.security.NoSuchAlgorithmException;
42import java.security.PublicKey;
43import java.security.SecureRandom;
44import java.security.UnrecoverableKeyException;
45import java.util.ArrayList;
46import java.util.List;
47import java.util.Map;
48
49import javax.crypto.KeyGenerator;
50import javax.crypto.NoSuchPaddingException;
51import javax.crypto.SecretKey;
52
53/**
54 * Task to sync application keys to a remote vault service.
55 *
56 * @hide
57 */
58public class KeySyncTask implements Runnable {
59    private static final String TAG = "KeySyncTask";
60
61    private static final String RECOVERY_KEY_ALGORITHM = "AES";
62    private static final int RECOVERY_KEY_SIZE_BITS = 256;
63    private static final int SALT_LENGTH_BYTES = 16;
64    private static final int LENGTH_PREFIX_BYTES = Integer.BYTES;
65    private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
66    private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10;
67
68    private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
69    private final int mUserId;
70    private final int mCredentialType;
71    private final String mCredential;
72    private final boolean mCredentialUpdated;
73    private final PlatformKeyManager.Factory mPlatformKeyManagerFactory;
74    private final RecoverySnapshotStorage mRecoverySnapshotStorage;
75    private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
76
77    public static KeySyncTask newInstance(
78            Context context,
79            RecoverableKeyStoreDb recoverableKeyStoreDb,
80            RecoverySnapshotStorage snapshotStorage,
81            RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
82            int userId,
83            int credentialType,
84            String credential,
85            boolean credentialUpdated
86    ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
87        return new KeySyncTask(
88                recoverableKeyStoreDb,
89                snapshotStorage,
90                recoverySnapshotListenersStorage,
91                userId,
92                credentialType,
93                credential,
94                credentialUpdated,
95                () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb));
96    }
97
98    /**
99     * A new task.
100     *
101     * @param recoverableKeyStoreDb Database where the keys are stored.
102     * @param userId The uid of the user whose profile has been unlocked.
103     * @param credentialType The type of credential - i.e., pattern or password.
104     * @param credential The credential, encoded as a {@link String}.
105     * @param credentialUpdated signals weather credentials were updated.
106     * @param platformKeyManagerFactory Instantiates a {@link PlatformKeyManager} for the user.
107     *     This is a factory to enable unit testing, as otherwise it would be impossible to test
108     *     without a screen unlock occurring!
109     */
110    @VisibleForTesting
111    KeySyncTask(
112            RecoverableKeyStoreDb recoverableKeyStoreDb,
113            RecoverySnapshotStorage snapshotStorage,
114            RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
115            int userId,
116            int credentialType,
117            String credential,
118            boolean credentialUpdated,
119            PlatformKeyManager.Factory platformKeyManagerFactory) {
120        mSnapshotListenersStorage = recoverySnapshotListenersStorage;
121        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
122        mUserId = userId;
123        mCredentialType = credentialType;
124        mCredential = credential;
125        mCredentialUpdated = credentialUpdated;
126        mPlatformKeyManagerFactory = platformKeyManagerFactory;
127        mRecoverySnapshotStorage = snapshotStorage;
128    }
129
130    @Override
131    public void run() {
132        try {
133            // Only one task is active If user unlocks phone many times in a short time interval.
134            synchronized(KeySyncTask.class) {
135                syncKeys();
136            }
137        } catch (Exception e) {
138            Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
139        }
140    }
141
142    private void syncKeys() {
143        if (mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
144            // Application keys for the user will not be available for sync.
145            Log.w(TAG, "Credentials are not set for user " + mUserId);
146            return;
147        }
148
149        List<Integer> recoveryAgents = mRecoverableKeyStoreDb.getRecoveryAgents(mUserId);
150        for (int uid : recoveryAgents) {
151            syncKeysForAgent(uid);
152        }
153        if (recoveryAgents.isEmpty()) {
154            Log.w(TAG, "No recovery agent initialized for user " + mUserId);
155        }
156    }
157
158    private void syncKeysForAgent(int recoveryAgentUid) {
159        if (!shoudCreateSnapshot(recoveryAgentUid)) {
160            Log.d(TAG, "Key sync not needed.");
161            return;
162        }
163
164        if (!mSnapshotListenersStorage.hasListener(recoveryAgentUid)) {
165            Log.w(TAG, "No pending intent registered for recovery agent " + recoveryAgentUid);
166            return;
167        }
168
169        PublicKey publicKey = mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId,
170                recoveryAgentUid);
171        if (publicKey == null) {
172            Log.w(TAG, "Not initialized for KeySync: no public key set. Cancelling task.");
173            return;
174        }
175
176        Long deviceId = mRecoverableKeyStoreDb.getServerParameters(mUserId, recoveryAgentUid);
177        if (deviceId == null) {
178            Log.w(TAG, "No device ID set for user " + mUserId);
179            return;
180        }
181
182        byte[] salt = generateSalt();
183        byte[] localLskfHash = hashCredentials(salt, mCredential);
184
185        Map<String, SecretKey> rawKeys;
186        try {
187            rawKeys = getKeysToSync(recoveryAgentUid);
188        } catch (GeneralSecurityException e) {
189            Log.e(TAG, "Failed to load recoverable keys for sync", e);
190            return;
191        } catch (InsecureUserException e) {
192            Log.wtf(TAG, "A screen unlock triggered the key sync flow, so user must have "
193                    + "lock screen. This should be impossible.", e);
194            return;
195        } catch (BadPlatformKeyException e) {
196            Log.wtf(TAG, "Loaded keys for same generation ID as platform key, so "
197                    + "BadPlatformKeyException should be impossible.", e);
198            return;
199        }
200
201        SecretKey recoveryKey;
202        try {
203            recoveryKey = generateRecoveryKey();
204        } catch (NoSuchAlgorithmException e) {
205            Log.wtf("AES should never be unavailable", e);
206            return;
207        }
208
209        Map<String, byte[]> encryptedApplicationKeys;
210        try {
211            encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey(
212                    recoveryKey, rawKeys);
213        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
214            Log.wtf(TAG,
215                    "Should be impossible: could not encrypt application keys with random key",
216                    e);
217            return;
218        }
219
220        Long counterId;
221        // counter id is generated exactly once for each credentials value.
222        if (mCredentialUpdated) {
223            counterId = generateAndStoreCounterId(recoveryAgentUid);
224        } else {
225            counterId = mRecoverableKeyStoreDb.getCounterId(mUserId, recoveryAgentUid);
226            if (counterId == null) {
227                counterId = generateAndStoreCounterId(recoveryAgentUid);
228            }
229        }
230        byte[] vaultParams = KeySyncUtils.packVaultParams(
231                publicKey,
232                counterId,
233                TRUSTED_HARDWARE_MAX_ATTEMPTS,
234                deviceId);
235
236        byte[] encryptedRecoveryKey;
237        try {
238            encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
239                    publicKey,
240                    localLskfHash,
241                    vaultParams,
242                    recoveryKey);
243        } catch (NoSuchAlgorithmException e) {
244            Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
245            return;
246        } catch (InvalidKeyException e) {
247            Log.e(TAG,"Could not encrypt with recovery key", e);
248            return;
249        }
250        // TODO: store raw data in RecoveryServiceMetadataEntry and generate Parcelables later
251        KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata(
252                /*userSecretType=*/ TYPE_LOCKSCREEN,
253                /*lockScreenUiFormat=*/ mCredentialType,
254                /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt),
255                /*secret=*/ new byte[0]);
256        ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>();
257        metadataList.add(metadata);
258
259        int snapshotVersion = incrementSnapshotVersion(recoveryAgentUid);
260
261        // If application keys are not updated, snapshot will not be created on next unlock.
262        mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, false);
263
264        mRecoverySnapshotStorage.put(recoveryAgentUid, new KeyStoreRecoveryData(
265                snapshotVersion,
266                /*recoveryMetadata=*/ metadataList,
267                /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys),
268                /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey));
269
270        mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
271    }
272
273    @VisibleForTesting
274    int incrementSnapshotVersion(int recoveryAgentUid) {
275        Long snapshotVersion = mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid);
276        snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion + 1;
277        mRecoverableKeyStoreDb.setSnapshotVersion(mUserId, recoveryAgentUid, snapshotVersion);
278
279        return snapshotVersion.intValue();
280    }
281
282    private long generateAndStoreCounterId(int recoveryAgentUid) {
283        long counter = new SecureRandom().nextLong();
284        mRecoverableKeyStoreDb.setCounterId(mUserId, recoveryAgentUid, counter);
285        return counter;
286    }
287
288    /**
289     * Returns all of the recoverable keys for the user.
290     */
291    private Map<String, SecretKey> getKeysToSync(int recoveryAgentUid)
292            throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
293            NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
294        PlatformKeyManager platformKeyManager = mPlatformKeyManagerFactory.newInstance();
295        PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey(mUserId);
296        Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
297                mUserId, recoveryAgentUid, decryptKey.getGenerationId());
298        return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
299    }
300
301    /**
302     * Returns {@code true} if a sync is pending.
303     * @param recoveryAgentUid uid of the recovery agent.
304     */
305    private boolean shoudCreateSnapshot(int recoveryAgentUid) {
306        if (mCredentialUpdated) {
307            // Sync credential if at least one snapshot was created.
308            if (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) {
309                mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, true);
310                return true;
311            }
312        }
313
314        return mRecoverableKeyStoreDb.getShouldCreateSnapshot(mUserId, recoveryAgentUid);
315    }
316
317    /**
318     * The UI best suited to entering the given lock screen. This is synced with the vault so the
319     * user can be shown the same UI when recovering the vault on another device.
320     *
321     * @return The format - either pattern, pin, or password.
322     */
323    @VisibleForTesting
324    @KeyStoreRecoveryMetadata.LockScreenUiFormat static int getUiFormat(
325            int credentialType, String credential) {
326        if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
327            return KeyStoreRecoveryMetadata.TYPE_PATTERN;
328        } else if (isPin(credential)) {
329            return KeyStoreRecoveryMetadata.TYPE_PIN;
330        } else {
331            return KeyStoreRecoveryMetadata.TYPE_PASSWORD;
332        }
333    }
334
335    /**
336     * Generates a salt to include with the lock screen hash.
337     *
338     * @return The salt.
339     */
340    private byte[] generateSalt() {
341        byte[] salt = new byte[SALT_LENGTH_BYTES];
342        new SecureRandom().nextBytes(salt);
343        return salt;
344    }
345
346    /**
347     * Returns {@code true} if {@code credential} looks like a pin.
348     */
349    @VisibleForTesting
350    static boolean isPin(@NonNull String credential) {
351        int length = credential.length();
352        for (int i = 0; i < length; i++) {
353            if (!Character.isDigit(credential.charAt(i))) {
354                return false;
355            }
356        }
357        return true;
358    }
359
360    /**
361     * Hashes {@code credentials} with the given {@code salt}.
362     *
363     * @return The SHA-256 hash.
364     */
365    @VisibleForTesting
366    static byte[] hashCredentials(byte[] salt, String credentials) {
367        byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
368        ByteBuffer byteBuffer = ByteBuffer.allocate(
369                salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
370        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
371        byteBuffer.putInt(salt.length);
372        byteBuffer.put(salt);
373        byteBuffer.putInt(credentialsBytes.length);
374        byteBuffer.put(credentialsBytes);
375        byte[] bytes = byteBuffer.array();
376
377        try {
378            return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
379        } catch (NoSuchAlgorithmException e) {
380            // Impossible, SHA-256 must be supported on Android.
381            throw new RuntimeException(e);
382        }
383    }
384
385    private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
386        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
387        keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
388        return keyGenerator.generateKey();
389    }
390
391    private static List<KeyEntryRecoveryData> createApplicationKeyEntries(
392            Map<String, byte[]> encryptedApplicationKeys) {
393        ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>();
394        for (String alias : encryptedApplicationKeys.keySet()) {
395            keyEntries.add(
396                    new KeyEntryRecoveryData(
397                            alias,
398                            encryptedApplicationKeys.get(alias)));
399        }
400        return keyEntries;
401    }
402}
403