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 com.android.internal.annotations.VisibleForTesting;
20
21import java.nio.ByteBuffer;
22import java.nio.ByteOrder;
23import java.nio.charset.StandardCharsets;
24import java.security.InvalidKeyException;
25import java.security.KeyFactory;
26import java.security.MessageDigest;
27import java.security.NoSuchAlgorithmException;
28import java.security.PublicKey;
29import java.security.SecureRandom;
30import java.security.spec.InvalidKeySpecException;
31import java.security.spec.X509EncodedKeySpec;
32import java.util.HashMap;
33import java.util.Map;
34
35import javax.crypto.AEADBadTagException;
36import javax.crypto.KeyGenerator;
37import javax.crypto.SecretKey;
38
39/**
40 * Utility functions for the flow where the RecoveryController syncs keys with remote storage.
41 *
42 * @hide
43 */
44public class KeySyncUtils {
45
46    private static final String PUBLIC_KEY_FACTORY_ALGORITHM = "EC";
47    private static final String RECOVERY_KEY_ALGORITHM = "AES";
48    private static final int RECOVERY_KEY_SIZE_BITS = 256;
49
50    private static final byte[] THM_ENCRYPTED_RECOVERY_KEY_HEADER =
51            "V1 THM_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
52    private static final byte[] LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER =
53            "V1 locally_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
54    private static final byte[] ENCRYPTED_APPLICATION_KEY_HEADER =
55            "V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
56    private static final byte[] RECOVERY_CLAIM_HEADER =
57            "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
58    private static final byte[] RECOVERY_RESPONSE_HEADER =
59            "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
60
61    private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);
62
63    private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
64
65    /**
66     * Encrypts the recovery key using both the lock screen hash and the remote storage's public
67     * key.
68     *
69     * @param publicKey The public key of the remote storage.
70     * @param lockScreenHash The user's lock screen hash.
71     * @param vaultParams Additional parameters to send to the remote storage.
72     * @param recoveryKey The recovery key.
73     * @return The encrypted bytes.
74     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
75     * @throws InvalidKeyException if the public key or the lock screen could not be used to encrypt
76     *     the data.
77     *
78     * @hide
79     */
80    public static byte[] thmEncryptRecoveryKey(
81            PublicKey publicKey,
82            byte[] lockScreenHash,
83            byte[] vaultParams,
84            SecretKey recoveryKey
85    ) throws NoSuchAlgorithmException, InvalidKeyException {
86        byte[] encryptedRecoveryKey = locallyEncryptRecoveryKey(lockScreenHash, recoveryKey);
87        byte[] thmKfHash = calculateThmKfHash(lockScreenHash);
88        byte[] header = concat(THM_ENCRYPTED_RECOVERY_KEY_HEADER, vaultParams);
89        return SecureBox.encrypt(
90                /*theirPublicKey=*/ publicKey,
91                /*sharedSecret=*/ thmKfHash,
92                /*header=*/ header,
93                /*payload=*/ encryptedRecoveryKey);
94    }
95
96    /**
97     * Calculates the THM_KF hash of the lock screen hash.
98     *
99     * @param lockScreenHash The lock screen hash.
100     * @return The hash.
101     * @throws NoSuchAlgorithmException if SHA-256 is unavailable (should never happen).
102     *
103     * @hide
104     */
105    public static byte[] calculateThmKfHash(byte[] lockScreenHash)
106            throws NoSuchAlgorithmException {
107        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
108        messageDigest.update(THM_KF_HASH_PREFIX);
109        messageDigest.update(lockScreenHash);
110        return messageDigest.digest();
111    }
112
113    /**
114     * Encrypts the recovery key using the lock screen hash.
115     *
116     * @param lockScreenHash The raw lock screen hash.
117     * @param recoveryKey The recovery key.
118     * @return The encrypted bytes.
119     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
120     * @throws InvalidKeyException if the hash cannot be used to encrypt for some reason.
121     */
122    @VisibleForTesting
123    static byte[] locallyEncryptRecoveryKey(byte[] lockScreenHash, SecretKey recoveryKey)
124            throws NoSuchAlgorithmException, InvalidKeyException {
125        return SecureBox.encrypt(
126                /*theirPublicKey=*/ null,
127                /*sharedSecret=*/ lockScreenHash,
128                /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
129                /*payload=*/ recoveryKey.getEncoded());
130    }
131
132    /**
133     * Returns a new random 256-bit AES recovery key.
134     *
135     * @hide
136     */
137    public static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
138        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
139        keyGenerator.init(RECOVERY_KEY_SIZE_BITS, new SecureRandom());
140        return keyGenerator.generateKey();
141    }
142
143    /**
144     * Encrypts all of the given keys with the recovery key, using SecureBox.
145     *
146     * @param recoveryKey The recovery key.
147     * @param keys The keys, indexed by their aliases.
148     * @return The encrypted key material, indexed by aliases.
149     * @throws NoSuchAlgorithmException if any of the SecureBox algorithms are unavailable.
150     * @throws InvalidKeyException if the recovery key is not appropriate for encrypting the keys.
151     *
152     * @hide
153     */
154    public static Map<String, byte[]> encryptKeysWithRecoveryKey(
155            SecretKey recoveryKey, Map<String, SecretKey> keys)
156            throws NoSuchAlgorithmException, InvalidKeyException {
157        HashMap<String, byte[]> encryptedKeys = new HashMap<>();
158        for (String alias : keys.keySet()) {
159            SecretKey key = keys.get(alias);
160            byte[] encryptedKey = SecureBox.encrypt(
161                    /*theirPublicKey=*/ null,
162                    /*sharedSecret=*/ recoveryKey.getEncoded(),
163                    /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
164                    /*payload=*/ key.getEncoded());
165            encryptedKeys.put(alias, encryptedKey);
166        }
167        return encryptedKeys;
168    }
169
170    /**
171     * Returns a random 16-byte key claimant.
172     *
173     * @hide
174     */
175    public static byte[] generateKeyClaimant() {
176        SecureRandom secureRandom = new SecureRandom();
177        byte[] key = new byte[KEY_CLAIMANT_LENGTH_BYTES];
178        secureRandom.nextBytes(key);
179        return key;
180    }
181
182    /**
183     * Encrypts a claim to recover a remote recovery key.
184     *
185     * @param publicKey The public key of the remote server.
186     * @param vaultParams Associated vault parameters.
187     * @param challenge The challenge issued by the server.
188     * @param thmKfHash The THM hash of the lock screen.
189     * @param keyClaimant The random key claimant.
190     * @return The encrypted recovery claim, to be sent to the remote server.
191     * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
192     * @throws InvalidKeyException if the {@code publicKey} could not be used to encrypt.
193     *
194     * @hide
195     */
196    public static byte[] encryptRecoveryClaim(
197            PublicKey publicKey,
198            byte[] vaultParams,
199            byte[] challenge,
200            byte[] thmKfHash,
201            byte[] keyClaimant) throws NoSuchAlgorithmException, InvalidKeyException {
202        return SecureBox.encrypt(
203                publicKey,
204                /*sharedSecret=*/ null,
205                /*header=*/ concat(RECOVERY_CLAIM_HEADER, vaultParams, challenge),
206                /*payload=*/ concat(thmKfHash, keyClaimant));
207    }
208
209    /**
210     * Decrypts response from recovery claim, returning the locally encrypted key.
211     *
212     * @param keyClaimant The key claimant, used by the remote service to encrypt the response.
213     * @param vaultParams Vault params associated with the claim.
214     * @param encryptedResponse The encrypted response.
215     * @return The locally encrypted recovery key.
216     * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
217     * @throws InvalidKeyException if the {@code keyClaimant} could not be used to decrypt.
218     * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
219     *     different key.
220     */
221    public static byte[] decryptRecoveryClaimResponse(
222            byte[] keyClaimant, byte[] vaultParams, byte[] encryptedResponse)
223            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
224        return SecureBox.decrypt(
225                /*ourPrivateKey=*/ null,
226                /*sharedSecret=*/ keyClaimant,
227                /*header=*/ concat(RECOVERY_RESPONSE_HEADER, vaultParams),
228                /*encryptedPayload=*/ encryptedResponse);
229    }
230
231    /**
232     * Decrypts a recovery key, after having retrieved it from a remote server.
233     *
234     * @param lskfHash The lock screen hash associated with the key.
235     * @param encryptedRecoveryKey The encrypted key.
236     * @return The raw key material.
237     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
238     * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
239     *     different key.
240     */
241    public static byte[] decryptRecoveryKey(byte[] lskfHash, byte[] encryptedRecoveryKey)
242            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
243        return SecureBox.decrypt(
244                /*ourPrivateKey=*/ null,
245                /*sharedSecret=*/ lskfHash,
246                /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
247                /*encryptedPayload=*/ encryptedRecoveryKey);
248    }
249
250    /**
251     * Decrypts an application key, using the recovery key.
252     *
253     * @param recoveryKey The recovery key - used to wrap all application keys.
254     * @param encryptedApplicationKey The application key to unwrap.
255     * @return The raw key material of the application key.
256     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
257     * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
258     *     different key.
259     */
260    public static byte[] decryptApplicationKey(byte[] recoveryKey, byte[] encryptedApplicationKey)
261            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
262        return SecureBox.decrypt(
263                /*ourPrivateKey=*/ null,
264                /*sharedSecret=*/ recoveryKey,
265                /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
266                /*encryptedPayload=*/ encryptedApplicationKey);
267    }
268
269    /**
270     * Deserializes a X509 public key.
271     *
272     * @param key The bytes of the key.
273     * @return The key.
274     * @throws InvalidKeySpecException if the bytes of the key are not a valid key.
275     */
276    public static PublicKey deserializePublicKey(byte[] key) throws InvalidKeySpecException {
277        KeyFactory keyFactory;
278        try {
279            keyFactory = KeyFactory.getInstance(PUBLIC_KEY_FACTORY_ALGORITHM);
280        } catch (NoSuchAlgorithmException e) {
281            // Should not happen
282            throw new RuntimeException(e);
283        }
284        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(key);
285        return keyFactory.generatePublic(publicKeySpec);
286    }
287
288    /**
289     * Packs vault params into a binary format.
290     *
291     * @param thmPublicKey Public key of the trusted hardware module.
292     * @param counterId ID referring to the specific counter in the hardware module.
293     * @param maxAttempts Maximum allowed guesses before trusted hardware wipes key.
294     * @param vaultHandle Handle of the Vault.
295     * @return The binary vault params, ready for sync.
296     */
297    public static byte[] packVaultParams(
298            PublicKey thmPublicKey, long counterId, int maxAttempts, byte[] vaultHandle) {
299        int vaultParamsLength
300                = 65 // public key
301                + 8 // counterId
302                + 4 // maxAttempts
303                + vaultHandle.length;
304        return ByteBuffer.allocate(vaultParamsLength)
305                .order(ByteOrder.LITTLE_ENDIAN)
306                .put(SecureBox.encodePublicKey(thmPublicKey))
307                .putLong(counterId)
308                .putInt(maxAttempts)
309                .put(vaultHandle)
310                .array();
311    }
312
313    /**
314     * Returns the concatenation of all the given {@code arrays}.
315     */
316    @VisibleForTesting
317    static byte[] concat(byte[]... arrays) {
318        int length = 0;
319        for (byte[] array : arrays) {
320            length += array.length;
321        }
322
323        byte[] concatenated = new byte[length];
324        int pos = 0;
325        for (byte[] array : arrays) {
326            System.arraycopy(array, /*srcPos=*/ 0, concatenated, pos, array.length);
327            pos += array.length;
328        }
329
330        return concatenated;
331    }
332
333    // Statics only
334    private KeySyncUtils() {}
335}
336