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 com.android.internal.annotations.VisibleForTesting; 21 import java.math.BigInteger; 22 import java.nio.BufferUnderflowException; 23 import java.nio.ByteBuffer; 24 import java.nio.charset.StandardCharsets; 25 import java.security.InvalidAlgorithmParameterException; 26 import java.security.InvalidKeyException; 27 import java.security.KeyFactory; 28 import java.security.KeyPair; 29 import java.security.KeyPairGenerator; 30 import java.security.NoSuchAlgorithmException; 31 import java.security.PrivateKey; 32 import java.security.PublicKey; 33 import java.security.SecureRandom; 34 import java.security.interfaces.ECPublicKey; 35 import java.security.spec.ECFieldFp; 36 import java.security.spec.ECGenParameterSpec; 37 import java.security.spec.ECParameterSpec; 38 import java.security.spec.ECPoint; 39 import java.security.spec.ECPublicKeySpec; 40 import java.security.spec.EllipticCurve; 41 import java.security.spec.InvalidKeySpecException; 42 import java.util.Arrays; 43 import javax.crypto.AEADBadTagException; 44 import javax.crypto.BadPaddingException; 45 import javax.crypto.Cipher; 46 import javax.crypto.IllegalBlockSizeException; 47 import javax.crypto.KeyAgreement; 48 import javax.crypto.Mac; 49 import javax.crypto.NoSuchPaddingException; 50 import javax.crypto.SecretKey; 51 import javax.crypto.spec.GCMParameterSpec; 52 import javax.crypto.spec.SecretKeySpec; 53 54 /** 55 * Implementation of the SecureBox v2 crypto functions. 56 * 57 * <p>Securebox v2 provides a simple interface to perform encryptions by using any of the following 58 * credential types: 59 * 60 * <ul> 61 * <li>A public key owned by the recipient, 62 * <li>A secret shared between the sender and the recipient, or 63 * <li>Both a recipient's public key and a shared secret. 64 * </ul> 65 * 66 * @hide 67 */ 68 public class SecureBox { 69 70 private static final byte[] VERSION = new byte[] {(byte) 0x02, 0}; // LITTLE_ENDIAN_TWO_BYTES(2) 71 private static final byte[] HKDF_SALT = 72 concat("SECUREBOX".getBytes(StandardCharsets.UTF_8), VERSION); 73 private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY = 74 "P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); 75 private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY = 76 "SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); 77 private static final byte[] CONSTANT_01 = {(byte) 0x01}; 78 private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; 79 private static final byte EC_PUBLIC_KEY_PREFIX = (byte) 0x04; 80 81 private static final String CIPHER_ALG = "AES"; 82 private static final String EC_ALG = "EC"; 83 private static final String EC_P256_COMMON_NAME = "secp256r1"; 84 private static final String EC_P256_OPENSSL_NAME = "prime256v1"; 85 private static final String ENC_ALG = "AES/GCM/NoPadding"; 86 private static final String KA_ALG = "ECDH"; 87 private static final String MAC_ALG = "HmacSHA256"; 88 89 private static final int EC_COORDINATE_LEN_BYTES = 32; 90 private static final int EC_PUBLIC_KEY_LEN_BYTES = 2 * EC_COORDINATE_LEN_BYTES + 1; 91 private static final int GCM_NONCE_LEN_BYTES = 12; 92 private static final int GCM_KEY_LEN_BYTES = 16; 93 private static final int GCM_TAG_LEN_BYTES = 16; 94 95 private static final BigInteger BIG_INT_02 = BigInteger.valueOf(2); 96 97 private enum AesGcmOperation { 98 ENCRYPT, 99 DECRYPT 100 } 101 102 // Parameters for the NIST P-256 curve y^2 = x^3 + ax + b (mod p) 103 private static final BigInteger EC_PARAM_P = 104 new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16); 105 private static final BigInteger EC_PARAM_A = EC_PARAM_P.subtract(new BigInteger("3")); 106 private static final BigInteger EC_PARAM_B = 107 new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16); 108 109 @VisibleForTesting static final ECParameterSpec EC_PARAM_SPEC; 110 111 static { 112 EllipticCurve curveSpec = 113 new EllipticCurve(new ECFieldFp(EC_PARAM_P), EC_PARAM_A, EC_PARAM_B); 114 ECPoint generator = 115 new ECPoint( 116 new BigInteger( 117 "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", 118 16), 119 new BigInteger( 120 "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", 121 16)); 122 BigInteger generatorOrder = 123 new BigInteger( 124 "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16); 125 EC_PARAM_SPEC = new ECParameterSpec(curveSpec, generator, generatorOrder, /* cofactor */ 1); 126 } 127 SecureBox()128 private SecureBox() {} 129 130 /** 131 * Randomly generates a public-key pair that can be used for the functions {@link #encrypt} and 132 * {@link #decrypt}. 133 * 134 * @return the randomly generated public-key pair 135 * @throws NoSuchAlgorithmException if the underlying crypto algorithm is not supported 136 * @hide 137 */ genKeyPair()138 public static KeyPair genKeyPair() throws NoSuchAlgorithmException { 139 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALG); 140 try { 141 // Try using the OpenSSL provider first 142 keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME)); 143 return keyPairGenerator.generateKeyPair(); 144 } catch (InvalidAlgorithmParameterException ex) { 145 // Try another name for NIST P-256 146 } 147 try { 148 keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME)); 149 return keyPairGenerator.generateKeyPair(); 150 } catch (InvalidAlgorithmParameterException ex) { 151 throw new NoSuchAlgorithmException("Unable to find the NIST P-256 curve", ex); 152 } 153 } 154 155 /** 156 * Encrypts {@code payload} by using {@code theirPublicKey} and/or {@code sharedSecret}. At 157 * least one of {@code theirPublicKey} and {@code sharedSecret} must be non-null, and an empty 158 * {@code sharedSecret} is equivalent to null. 159 * 160 * <p>Note that {@code header} will be authenticated (but not encrypted) together with {@code 161 * payload}, and the same {@code header} has to be provided for {@link #decrypt}. 162 * 163 * @param theirPublicKey the recipient's public key, or null if the payload is to be encrypted 164 * only with the shared secret 165 * @param sharedSecret the secret shared between the sender and the recipient, or null if the 166 * payload is to be encrypted only with the recipient's public key 167 * @param header the data that will be authenticated with {@code payload} but not encrypted, or 168 * null if the data is empty 169 * @param payload the data to be encrypted, or null if the data is empty 170 * @return the encrypted payload 171 * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported 172 * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms 173 * @hide 174 */ encrypt( @ullable PublicKey theirPublicKey, @Nullable byte[] sharedSecret, @Nullable byte[] header, @Nullable byte[] payload)175 public static byte[] encrypt( 176 @Nullable PublicKey theirPublicKey, 177 @Nullable byte[] sharedSecret, 178 @Nullable byte[] header, 179 @Nullable byte[] payload) 180 throws NoSuchAlgorithmException, InvalidKeyException { 181 sharedSecret = emptyByteArrayIfNull(sharedSecret); 182 if (theirPublicKey == null && sharedSecret.length == 0) { 183 throw new IllegalArgumentException("Both the public key and shared secret are empty"); 184 } 185 header = emptyByteArrayIfNull(header); 186 payload = emptyByteArrayIfNull(payload); 187 188 KeyPair senderKeyPair; 189 byte[] dhSecret; 190 byte[] hkdfInfo; 191 if (theirPublicKey == null) { 192 senderKeyPair = null; 193 dhSecret = EMPTY_BYTE_ARRAY; 194 hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; 195 } else { 196 senderKeyPair = genKeyPair(); 197 dhSecret = dhComputeSecret(senderKeyPair.getPrivate(), theirPublicKey); 198 hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; 199 } 200 201 byte[] randNonce = genRandomNonce(); 202 byte[] keyingMaterial = concat(dhSecret, sharedSecret); 203 SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); 204 byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header); 205 if (senderKeyPair == null) { 206 return concat(VERSION, randNonce, ciphertext); 207 } else { 208 return concat( 209 VERSION, encodePublicKey(senderKeyPair.getPublic()), randNonce, ciphertext); 210 } 211 } 212 213 /** 214 * Decrypts {@code encryptedPayload} by using {@code ourPrivateKey} and/or {@code sharedSecret}. 215 * At least one of {@code ourPrivateKey} and {@code sharedSecret} must be non-null, and an empty 216 * {@code sharedSecret} is equivalent to null. 217 * 218 * <p>Note that {@code header} should be the same data used for {@link #encrypt}, which is 219 * authenticated (but not encrypted) together with {@code payload}; otherwise, an {@code 220 * AEADBadTagException} will be thrown. 221 * 222 * @param ourPrivateKey the recipient's private key, or null if the payload was encrypted only 223 * with the shared secret 224 * @param sharedSecret the secret shared between the sender and the recipient, or null if the 225 * payload was encrypted only with the recipient's public key 226 * @param header the data that was authenticated with the original payload but not encrypted, or 227 * null if the data is empty 228 * @param encryptedPayload the data to be decrypted 229 * @return the original payload that was encrypted 230 * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported 231 * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms 232 * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload} 233 * cannot be validated, or if the payload is not a valid SecureBox V2 payload. 234 * @hide 235 */ decrypt( @ullable PrivateKey ourPrivateKey, @Nullable byte[] sharedSecret, @Nullable byte[] header, byte[] encryptedPayload)236 public static byte[] decrypt( 237 @Nullable PrivateKey ourPrivateKey, 238 @Nullable byte[] sharedSecret, 239 @Nullable byte[] header, 240 byte[] encryptedPayload) 241 throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { 242 sharedSecret = emptyByteArrayIfNull(sharedSecret); 243 if (ourPrivateKey == null && sharedSecret.length == 0) { 244 throw new IllegalArgumentException("Both the private key and shared secret are empty"); 245 } 246 header = emptyByteArrayIfNull(header); 247 if (encryptedPayload == null) { 248 throw new NullPointerException("Encrypted payload must not be null."); 249 } 250 251 ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload); 252 byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length); 253 if (!Arrays.equals(version, VERSION)) { 254 throw new AEADBadTagException("The payload was not encrypted by SecureBox v2"); 255 } 256 257 byte[] senderPublicKeyBytes; 258 byte[] dhSecret; 259 byte[] hkdfInfo; 260 if (ourPrivateKey == null) { 261 dhSecret = EMPTY_BYTE_ARRAY; 262 hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; 263 } else { 264 senderPublicKeyBytes = readEncryptedPayload(ciphertextBuffer, EC_PUBLIC_KEY_LEN_BYTES); 265 dhSecret = dhComputeSecret(ourPrivateKey, decodePublicKey(senderPublicKeyBytes)); 266 hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; 267 } 268 269 byte[] randNonce = readEncryptedPayload(ciphertextBuffer, GCM_NONCE_LEN_BYTES); 270 byte[] ciphertext = readEncryptedPayload(ciphertextBuffer, ciphertextBuffer.remaining()); 271 byte[] keyingMaterial = concat(dhSecret, sharedSecret); 272 SecretKey decryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); 273 return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header); 274 } 275 readEncryptedPayload(ByteBuffer buffer, int length)276 private static byte[] readEncryptedPayload(ByteBuffer buffer, int length) 277 throws AEADBadTagException { 278 byte[] output = new byte[length]; 279 try { 280 buffer.get(output); 281 } catch (BufferUnderflowException ex) { 282 throw new AEADBadTagException("The encrypted payload is too short"); 283 } 284 return output; 285 } 286 dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey)287 private static byte[] dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey) 288 throws NoSuchAlgorithmException, InvalidKeyException { 289 KeyAgreement agreement = KeyAgreement.getInstance(KA_ALG); 290 try { 291 agreement.init(ourPrivateKey); 292 } catch (RuntimeException ex) { 293 // Rethrow the RuntimeException as InvalidKeyException 294 throw new InvalidKeyException(ex); 295 } 296 agreement.doPhase(theirPublicKey, /*lastPhase=*/ true); 297 return agreement.generateSecret(); 298 } 299 300 /** Derives a 128-bit AES key. */ hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info)301 private static SecretKey hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info) 302 throws NoSuchAlgorithmException { 303 Mac mac = Mac.getInstance(MAC_ALG); 304 try { 305 mac.init(new SecretKeySpec(salt, MAC_ALG)); 306 } catch (InvalidKeyException ex) { 307 // This should never happen 308 throw new RuntimeException(ex); 309 } 310 byte[] pseudorandomKey = mac.doFinal(secret); 311 312 try { 313 mac.init(new SecretKeySpec(pseudorandomKey, MAC_ALG)); 314 } catch (InvalidKeyException ex) { 315 // This should never happen 316 throw new RuntimeException(ex); 317 } 318 mac.update(info); 319 // Hashing just one block will yield 256 bits, which is enough to construct the AES key 320 byte[] hkdfOutput = mac.doFinal(CONSTANT_01); 321 322 return new SecretKeySpec(Arrays.copyOf(hkdfOutput, GCM_KEY_LEN_BYTES), CIPHER_ALG); 323 } 324 aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad)325 private static byte[] aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad) 326 throws NoSuchAlgorithmException, InvalidKeyException { 327 try { 328 return aesGcmInternal(AesGcmOperation.ENCRYPT, key, nonce, plaintext, aad); 329 } catch (AEADBadTagException ex) { 330 // This should never happen 331 throw new RuntimeException(ex); 332 } 333 } 334 aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad)335 private static byte[] aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad) 336 throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { 337 return aesGcmInternal(AesGcmOperation.DECRYPT, key, nonce, ciphertext, aad); 338 } 339 aesGcmInternal( AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad)340 private static byte[] aesGcmInternal( 341 AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad) 342 throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { 343 Cipher cipher; 344 try { 345 cipher = Cipher.getInstance(ENC_ALG); 346 } catch (NoSuchPaddingException ex) { 347 // This should never happen because AES-GCM doesn't use padding 348 throw new RuntimeException(ex); 349 } 350 GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BYTES * 8, nonce); 351 try { 352 if (operation == AesGcmOperation.DECRYPT) { 353 cipher.init(Cipher.DECRYPT_MODE, key, spec); 354 } else { 355 cipher.init(Cipher.ENCRYPT_MODE, key, spec); 356 } 357 } catch (InvalidAlgorithmParameterException ex) { 358 // This should never happen 359 throw new RuntimeException(ex); 360 } 361 try { 362 cipher.updateAAD(aad); 363 return cipher.doFinal(text); 364 } catch (AEADBadTagException ex) { 365 // Catch and rethrow AEADBadTagException first because it's a subclass of 366 // BadPaddingException 367 throw ex; 368 } catch (IllegalBlockSizeException | BadPaddingException ex) { 369 // This should never happen because AES-GCM can handle inputs of any length without 370 // padding 371 throw new RuntimeException(ex); 372 } 373 } 374 375 /** 376 * Encodes public key in format expected by the secure hardware module. This is used as part 377 * of the vault params. 378 * 379 * @param publicKey The public key. 380 * @return The key packed into a 65-byte array. 381 */ encodePublicKey(PublicKey publicKey)382 static byte[] encodePublicKey(PublicKey publicKey) { 383 ECPoint point = ((ECPublicKey) publicKey).getW(); 384 byte[] x = point.getAffineX().toByteArray(); 385 byte[] y = point.getAffineY().toByteArray(); 386 387 byte[] output = new byte[EC_PUBLIC_KEY_LEN_BYTES]; 388 // The order of arraycopy() is important, because the coordinates may have a one-byte 389 // leading 0 for the sign bit of two's complement form 390 System.arraycopy(y, 0, output, EC_PUBLIC_KEY_LEN_BYTES - y.length, y.length); 391 System.arraycopy(x, 0, output, 1 + EC_COORDINATE_LEN_BYTES - x.length, x.length); 392 output[0] = EC_PUBLIC_KEY_PREFIX; 393 return output; 394 } 395 396 @VisibleForTesting decodePublicKey(byte[] keyBytes)397 static PublicKey decodePublicKey(byte[] keyBytes) 398 throws NoSuchAlgorithmException, InvalidKeyException { 399 BigInteger x = 400 new BigInteger( 401 /*signum=*/ 1, 402 Arrays.copyOfRange(keyBytes, 1, 1 + EC_COORDINATE_LEN_BYTES)); 403 BigInteger y = 404 new BigInteger( 405 /*signum=*/ 1, 406 Arrays.copyOfRange( 407 keyBytes, 1 + EC_COORDINATE_LEN_BYTES, EC_PUBLIC_KEY_LEN_BYTES)); 408 409 // Checks if the point is indeed on the P-256 curve for security considerations 410 validateEcPoint(x, y); 411 412 KeyFactory keyFactory = KeyFactory.getInstance(EC_ALG); 413 try { 414 return keyFactory.generatePublic(new ECPublicKeySpec(new ECPoint(x, y), EC_PARAM_SPEC)); 415 } catch (InvalidKeySpecException ex) { 416 // This should never happen 417 throw new RuntimeException(ex); 418 } 419 } 420 validateEcPoint(BigInteger x, BigInteger y)421 private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException { 422 if (x.compareTo(EC_PARAM_P) >= 0 423 || y.compareTo(EC_PARAM_P) >= 0 424 || x.signum() == -1 425 || y.signum() == -1) { 426 throw new InvalidKeyException("Point lies outside of the expected curve"); 427 } 428 429 // Points on the curve satisfy y^2 = x^3 + ax + b (mod p) 430 BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P); 431 BigInteger rhs = 432 x.modPow(BIG_INT_02, EC_PARAM_P) // x^2 433 .add(EC_PARAM_A) // x^2 + a 434 .mod(EC_PARAM_P) // This will speed up the next multiplication 435 .multiply(x) // (x^2 + a) * x = x^3 + ax 436 .add(EC_PARAM_B) // x^3 + ax + b 437 .mod(EC_PARAM_P); 438 if (!lhs.equals(rhs)) { 439 throw new InvalidKeyException("Point lies outside of the expected curve"); 440 } 441 } 442 genRandomNonce()443 private static byte[] genRandomNonce() throws NoSuchAlgorithmException { 444 byte[] nonce = new byte[GCM_NONCE_LEN_BYTES]; 445 new SecureRandom().nextBytes(nonce); 446 return nonce; 447 } 448 449 @VisibleForTesting concat(byte[]... inputs)450 static byte[] concat(byte[]... inputs) { 451 int length = 0; 452 for (int i = 0; i < inputs.length; i++) { 453 if (inputs[i] == null) { 454 inputs[i] = EMPTY_BYTE_ARRAY; 455 } 456 length += inputs[i].length; 457 } 458 459 byte[] output = new byte[length]; 460 int outputPos = 0; 461 for (byte[] input : inputs) { 462 System.arraycopy(input, /*srcPos=*/ 0, output, outputPos, input.length); 463 outputPos += input.length; 464 } 465 return output; 466 } 467 emptyByteArrayIfNull(@ullable byte[] input)468 private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) { 469 return input == null ? EMPTY_BYTE_ARRAY : input; 470 } 471 } 472