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