Alan Viverette | 3da604b | 2020-06-10 18:34:39 +0000 | [diff] [blame] | 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 | */ |
| 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 | */ |
| 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 |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 |
| 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 |
| 357 | private KeySyncUtils() {} |
| 358 | } |