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