KeySyncTask.java revision bd086f1963f13d13a03928f41b9b7979bebffa26
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
67    private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
68    private final int mUserId;
69    private final int mCredentialType;
70    private final String mCredential;
71    private final PlatformKeyManager.Factory mPlatformKeyManagerFactory;
72    private final VaultKeySupplier mVaultKeySupplier;
73    private final RecoverySnapshotStorage mRecoverySnapshotStorage;
74
75    public static KeySyncTask newInstance(
76            Context context,
77            RecoverableKeyStoreDb recoverableKeyStoreDb,
78            RecoverySnapshotStorage snapshotStorage,
79            int userId,
80            int credentialType,
81            String credential
82    ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
83        return new KeySyncTask(
84                recoverableKeyStoreDb,
85                snapshotStorage,
86                userId,
87                credentialType,
88                credential,
89                () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId),
90                () -> {
91                    throw new UnsupportedOperationException("Not implemented vault key.");
92                });
93    }
94
95    /**
96     * A new task.
97     *
98     * @param recoverableKeyStoreDb Database where the keys are stored.
99     * @param userId The uid of the user whose profile has been unlocked.
100     * @param credentialType The type of credential - i.e., pattern or password.
101     * @param credential The credential, encoded as a {@link String}.
102     * @param platformKeyManagerFactory Instantiates a {@link PlatformKeyManager} for the user.
103     *     This is a factory to enable unit testing, as otherwise it would be impossible to test
104     *     without a screen unlock occurring!
105     */
106    @VisibleForTesting
107    KeySyncTask(
108            RecoverableKeyStoreDb recoverableKeyStoreDb,
109            RecoverySnapshotStorage snapshotStorage,
110            int userId,
111            int credentialType,
112            String credential,
113            PlatformKeyManager.Factory platformKeyManagerFactory,
114            VaultKeySupplier vaultKeySupplier) {
115        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
116        mUserId = userId;
117        mCredentialType = credentialType;
118        mCredential = credential;
119        mPlatformKeyManagerFactory = platformKeyManagerFactory;
120        mVaultKeySupplier = vaultKeySupplier;
121        mRecoverySnapshotStorage = snapshotStorage;
122    }
123
124    @Override
125    public void run() {
126        try {
127            syncKeys();
128        } catch (Exception e) {
129            Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
130        }
131    }
132
133    private void syncKeys() {
134        if (!isSyncPending()) {
135            Log.d(TAG, "Key sync not needed.");
136            return;
137        }
138
139        byte[] salt = generateSalt();
140        byte[] localLskfHash = hashCredentials(salt, mCredential);
141
142        Map<String, SecretKey> rawKeys;
143        try {
144            rawKeys = getKeysToSync();
145        } catch (GeneralSecurityException e) {
146            Log.e(TAG, "Failed to load recoverable keys for sync", e);
147            return;
148        } catch (InsecureUserException e) {
149            Log.wtf(TAG, "A screen unlock triggered the key sync flow, so user must have "
150                    + "lock screen. This should be impossible.", e);
151            return;
152        } catch (BadPlatformKeyException e) {
153            Log.wtf(TAG, "Loaded keys for same generation ID as platform key, so "
154                    + "BadPlatformKeyException should be impossible.", e);
155            return;
156        }
157
158        SecretKey recoveryKey;
159        try {
160            recoveryKey = generateRecoveryKey();
161        } catch (NoSuchAlgorithmException e) {
162            Log.wtf("AES should never be unavailable", e);
163            return;
164        }
165
166        Map<String, byte[]> encryptedApplicationKeys;
167        try {
168            encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey(
169                    recoveryKey, rawKeys);
170        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
171            Log.wtf(TAG,
172                    "Should be impossible: could not encrypt application keys with random key",
173                    e);
174            return;
175        }
176
177        // TODO: construct vault params and vault metadata
178        byte[] vaultParams = {};
179
180        byte[] encryptedRecoveryKey;
181        try {
182            encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
183                    mVaultKeySupplier.get(),
184                    localLskfHash,
185                    vaultParams,
186                    recoveryKey);
187        } catch (NoSuchAlgorithmException e) {
188            Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
189            return;
190        } catch (InvalidKeyException e) {
191            Log.e(TAG,"Could not encrypt with recovery key", e);
192            return;
193        }
194
195        // TODO: why is the secret sent here? I thought it wasn't sent in the raw at all.
196        KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata(
197                /*userSecretType=*/ TYPE_LOCKSCREEN,
198                /*lockScreenUiFormat=*/ mCredentialType,
199                /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt),
200                /*secret=*/ new byte[0]);
201        ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>();
202        metadataList.add(metadata);
203
204        // TODO: implement snapshot version
205        mRecoverySnapshotStorage.put(mUserId, new KeyStoreRecoveryData(
206                /*snapshotVersion=*/ 1,
207                /*recoveryMetadata=*/ metadataList,
208                /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys),
209                /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey));
210    }
211
212    private PublicKey getVaultPublicKey() {
213        // TODO: fill this in
214        throw new UnsupportedOperationException("TODO: get vault public key.");
215    }
216
217    /**
218     * Returns all of the recoverable keys for the user.
219     */
220    private Map<String, SecretKey> getKeysToSync()
221            throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
222            NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
223        PlatformKeyManager platformKeyManager = mPlatformKeyManagerFactory.newInstance();
224        PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey();
225        Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
226                mUserId, decryptKey.getGenerationId());
227        return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
228    }
229
230    /**
231     * Returns {@code true} if a sync is pending.
232     */
233    private boolean isSyncPending() {
234        // TODO: implement properly. For now just always syncing if the user has any recoverable
235        // keys. We need to keep track of when the store's state actually changes.
236        return !mRecoverableKeyStoreDb.getAllKeys(
237                mUserId, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(mUserId)).isEmpty();
238    }
239
240    /**
241     * The UI best suited to entering the given lock screen. This is synced with the vault so the
242     * user can be shown the same UI when recovering the vault on another device.
243     *
244     * @return The format - either pattern, pin, or password.
245     */
246    @VisibleForTesting
247    @KeyStoreRecoveryMetadata.LockScreenUiFormat static int getUiFormat(
248            int credentialType, String credential) {
249        if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
250            return KeyStoreRecoveryMetadata.TYPE_PATTERN;
251        } else if (isPin(credential)) {
252            return KeyStoreRecoveryMetadata.TYPE_PIN;
253        } else {
254            return KeyStoreRecoveryMetadata.TYPE_PASSWORD;
255        }
256    }
257
258    /**
259     * Generates a salt to include with the lock screen hash.
260     *
261     * @return The salt.
262     */
263    private byte[] generateSalt() {
264        byte[] salt = new byte[SALT_LENGTH_BYTES];
265        new SecureRandom().nextBytes(salt);
266        return salt;
267    }
268
269    /**
270     * Returns {@code true} if {@code credential} looks like a pin.
271     */
272    @VisibleForTesting
273    static boolean isPin(@NonNull String credential) {
274        int length = credential.length();
275        for (int i = 0; i < length; i++) {
276            if (!Character.isDigit(credential.charAt(i))) {
277                return false;
278            }
279        }
280        return true;
281    }
282
283    /**
284     * Hashes {@code credentials} with the given {@code salt}.
285     *
286     * @return The SHA-256 hash.
287     */
288    @VisibleForTesting
289    static byte[] hashCredentials(byte[] salt, String credentials) {
290        byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
291        ByteBuffer byteBuffer = ByteBuffer.allocate(
292                salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
293        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
294        byteBuffer.putInt(salt.length);
295        byteBuffer.put(salt);
296        byteBuffer.putInt(credentialsBytes.length);
297        byteBuffer.put(credentialsBytes);
298        byte[] bytes = byteBuffer.array();
299
300        try {
301            return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
302        } catch (NoSuchAlgorithmException e) {
303            // Impossible, SHA-256 must be supported on Android.
304            throw new RuntimeException(e);
305        }
306    }
307
308    private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
309        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
310        keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
311        return keyGenerator.generateKey();
312    }
313
314    private static List<KeyEntryRecoveryData> createApplicationKeyEntries(
315            Map<String, byte[]> encryptedApplicationKeys) {
316        ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>();
317        for (String alias : encryptedApplicationKeys.keySet()) {
318            keyEntries.add(
319                    new KeyEntryRecoveryData(
320                            alias.getBytes(StandardCharsets.UTF_8),
321                            encryptedApplicationKeys.get(alias)));
322        }
323        return keyEntries;
324    }
325
326    /**
327     * TODO: until this is in the database, so we can test.
328     */
329    public interface VaultKeySupplier {
330        PublicKey get();
331    }
332}
333