KeySyncTask.java revision 4a534ecdd326db6557ac5fc275c11e091c306a44
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 android.annotation.NonNull;
20import android.content.Context;
21import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
22import android.util.Log;
23
24import com.android.internal.annotations.VisibleForTesting;
25import com.android.internal.widget.LockPatternUtils;
26import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
27
28import java.nio.ByteBuffer;
29import java.nio.ByteOrder;
30import java.nio.charset.StandardCharsets;
31import java.security.InvalidKeyException;
32import java.security.KeyStoreException;
33import java.security.MessageDigest;
34import java.security.NoSuchAlgorithmException;
35import java.security.PublicKey;
36import java.security.SecureRandom;
37
38import javax.crypto.KeyGenerator;
39import javax.crypto.SecretKey;
40
41/**
42 * Task to sync application keys to a remote vault service.
43 *
44 * TODO: implement fully
45 */
46public class KeySyncTask implements Runnable {
47    private static final String TAG = "KeySyncTask";
48
49    private static final String RECOVERY_KEY_ALGORITHM = "AES";
50    private static final int RECOVERY_KEY_SIZE_BITS = 256;
51    private static final int SALT_LENGTH_BYTES = 16;
52    private static final int LENGTH_PREFIX_BYTES = Integer.BYTES;
53    private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
54
55    private final Context mContext;
56    private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
57    private final int mUserId;
58    private final int mCredentialType;
59    private final String mCredential;
60
61    public static KeySyncTask newInstance(
62            Context context,
63            RecoverableKeyStoreDb recoverableKeyStoreDb,
64            int userId,
65            int credentialType,
66            String credential
67    ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
68        return new KeySyncTask(
69                context.getApplicationContext(),
70                recoverableKeyStoreDb,
71                userId,
72                credentialType,
73                credential);
74    }
75
76    /**
77     * A new task.
78     *
79     * @param recoverableKeyStoreDb Database where the keys are stored.
80     * @param userId The uid of the user whose profile has been unlocked.
81     * @param credentialType The type of credential - i.e., pattern or password.
82     * @param credential The credential, encoded as a {@link String}.
83     */
84    @VisibleForTesting
85    KeySyncTask(
86            Context context,
87            RecoverableKeyStoreDb recoverableKeyStoreDb,
88            int userId,
89            int credentialType,
90            String credential) {
91        mContext = context;
92        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
93        mUserId = userId;
94        mCredentialType = credentialType;
95        mCredential = credential;
96    }
97
98    @Override
99    public void run() {
100        try {
101            syncKeys();
102        } catch (Exception e) {
103            Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
104        }
105    }
106
107    private void syncKeys() {
108        byte[] salt = generateSalt();
109        byte[] localLskfHash = hashCredentials(salt, mCredential);
110
111        // TODO: decrypt local wrapped application keys, ready for sync
112
113        SecretKey recoveryKey;
114        try {
115            recoveryKey = generateRecoveryKey();
116        } catch (NoSuchAlgorithmException e) {
117            Log.wtf("AES should never be unavailable", e);
118            return;
119        }
120
121        // TODO: encrypt each application key with recovery key
122
123        PublicKey vaultKey = getVaultPublicKey();
124
125        // TODO: construct vault params and vault metadata
126        byte[] vaultParams = {};
127
128        byte[] locallyEncryptedRecoveryKey;
129        try {
130            locallyEncryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
131                    vaultKey,
132                    localLskfHash,
133                    vaultParams,
134                    recoveryKey);
135        } catch (NoSuchAlgorithmException e) {
136            Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
137            return;
138        } catch (InvalidKeyException e) {
139            Log.e(TAG,"Could not encrypt with recovery key", e);
140            return;
141        }
142
143        // TODO: send RECOVERABLE_KEYSTORE_SNAPSHOT intent
144    }
145
146    private PublicKey getVaultPublicKey() {
147        // TODO: fill this in
148        throw new UnsupportedOperationException("TODO: get vault public key.");
149    }
150
151    /**
152     * The UI best suited to entering the given lock screen. This is synced with the vault so the
153     * user can be shown the same UI when recovering the vault on another device.
154     *
155     * @return The format - either pattern, pin, or password.
156     */
157    @VisibleForTesting
158    @KeyStoreRecoveryMetadata.LockScreenUiFormat static int getUiFormat(
159            int credentialType, String credential) {
160        if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
161            return KeyStoreRecoveryMetadata.TYPE_PATTERN;
162        } else if (isPin(credential)) {
163            return KeyStoreRecoveryMetadata.TYPE_PIN;
164        } else {
165            return KeyStoreRecoveryMetadata.TYPE_PASSWORD;
166        }
167    }
168
169    /**
170     * Generates a salt to include with the lock screen hash.
171     *
172     * @return The salt.
173     */
174    private byte[] generateSalt() {
175        byte[] salt = new byte[SALT_LENGTH_BYTES];
176        new SecureRandom().nextBytes(salt);
177        return salt;
178    }
179
180    /**
181     * Returns {@code true} if {@code credential} looks like a pin.
182     */
183    @VisibleForTesting
184    static boolean isPin(@NonNull String credential) {
185        int length = credential.length();
186        for (int i = 0; i < length; i++) {
187            if (!Character.isDigit(credential.charAt(i))) {
188                return false;
189            }
190        }
191        return true;
192    }
193
194    /**
195     * Hashes {@code credentials} with the given {@code salt}.
196     *
197     * @return The SHA-256 hash.
198     */
199    @VisibleForTesting
200    static byte[] hashCredentials(byte[] salt, String credentials) {
201        byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
202        ByteBuffer byteBuffer = ByteBuffer.allocate(
203                salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
204        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
205        byteBuffer.putInt(salt.length);
206        byteBuffer.put(salt);
207        byteBuffer.putInt(credentialsBytes.length);
208        byteBuffer.put(credentialsBytes);
209        byte[] bytes = byteBuffer.array();
210
211        try {
212            return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
213        } catch (NoSuchAlgorithmException e) {
214            // Impossible, SHA-256 must be supported on Android.
215            throw new RuntimeException(e);
216        }
217    }
218
219    private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
220        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
221        keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
222        return keyGenerator.generateKey();
223    }
224}
225