| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.locksettings.recoverablekeystore; |
| |
| import android.annotation.Nullable; |
| import com.android.internal.annotations.VisibleForTesting; |
| import java.math.BigInteger; |
| import java.nio.BufferUnderflowException; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.StandardCharsets; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.KeyFactory; |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import java.security.PublicKey; |
| import java.security.SecureRandom; |
| import java.security.interfaces.ECPublicKey; |
| import java.security.spec.ECFieldFp; |
| import java.security.spec.ECGenParameterSpec; |
| import java.security.spec.ECParameterSpec; |
| import java.security.spec.ECPoint; |
| import java.security.spec.ECPublicKeySpec; |
| import java.security.spec.EllipticCurve; |
| import java.security.spec.InvalidKeySpecException; |
| import java.util.Arrays; |
| import javax.crypto.AEADBadTagException; |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.KeyAgreement; |
| import javax.crypto.Mac; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.SecretKey; |
| import javax.crypto.spec.GCMParameterSpec; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| /** |
| * Implementation of the SecureBox v2 crypto functions. |
| * |
| * <p>Securebox v2 provides a simple interface to perform encryptions by using any of the following |
| * credential types: |
| * |
| * <ul> |
| * <li>A public key owned by the recipient, |
| * <li>A secret shared between the sender and the recipient, or |
| * <li>Both a recipient's public key and a shared secret. |
| * </ul> |
| * |
| * @hide |
| */ |
| public class SecureBox { |
| |
| private static final byte[] VERSION = new byte[] {(byte) 0x02, 0}; // LITTLE_ENDIAN_TWO_BYTES(2) |
| private static final byte[] HKDF_SALT = |
| concat("SECUREBOX".getBytes(StandardCharsets.UTF_8), VERSION); |
| private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY = |
| "P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); |
| private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY = |
| "SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); |
| private static final byte[] CONSTANT_01 = {(byte) 0x01}; |
| private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; |
| private static final byte EC_PUBLIC_KEY_PREFIX = (byte) 0x04; |
| |
| private static final String CIPHER_ALG = "AES"; |
| private static final String EC_ALG = "EC"; |
| private static final String EC_P256_COMMON_NAME = "secp256r1"; |
| private static final String EC_P256_OPENSSL_NAME = "prime256v1"; |
| private static final String ENC_ALG = "AES/GCM/NoPadding"; |
| private static final String KA_ALG = "ECDH"; |
| private static final String MAC_ALG = "HmacSHA256"; |
| |
| private static final int EC_COORDINATE_LEN_BYTES = 32; |
| private static final int EC_PUBLIC_KEY_LEN_BYTES = 2 * EC_COORDINATE_LEN_BYTES + 1; |
| private static final int GCM_NONCE_LEN_BYTES = 12; |
| private static final int GCM_KEY_LEN_BYTES = 16; |
| private static final int GCM_TAG_LEN_BYTES = 16; |
| |
| private static final BigInteger BIG_INT_02 = BigInteger.valueOf(2); |
| |
| private enum AesGcmOperation { |
| ENCRYPT, |
| DECRYPT |
| } |
| |
| // Parameters for the NIST P-256 curve y^2 = x^3 + ax + b (mod p) |
| private static final BigInteger EC_PARAM_P = |
| new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16); |
| private static final BigInteger EC_PARAM_A = EC_PARAM_P.subtract(new BigInteger("3")); |
| private static final BigInteger EC_PARAM_B = |
| new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16); |
| |
| @VisibleForTesting static final ECParameterSpec EC_PARAM_SPEC; |
| |
| static { |
| EllipticCurve curveSpec = |
| new EllipticCurve(new ECFieldFp(EC_PARAM_P), EC_PARAM_A, EC_PARAM_B); |
| ECPoint generator = |
| new ECPoint( |
| new BigInteger( |
| "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", |
| 16), |
| new BigInteger( |
| "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", |
| 16)); |
| BigInteger generatorOrder = |
| new BigInteger( |
| "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16); |
| EC_PARAM_SPEC = new ECParameterSpec(curveSpec, generator, generatorOrder, /* cofactor */ 1); |
| } |
| |
| private SecureBox() {} |
| |
| /** |
| * Randomly generates a public-key pair that can be used for the functions {@link #encrypt} and |
| * {@link #decrypt}. |
| * |
| * @return the randomly generated public-key pair |
| * @throws NoSuchAlgorithmException if the underlying crypto algorithm is not supported |
| * @hide |
| */ |
| public static KeyPair genKeyPair() throws NoSuchAlgorithmException { |
| KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALG); |
| try { |
| // Try using the OpenSSL provider first |
| keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME)); |
| return keyPairGenerator.generateKeyPair(); |
| } catch (InvalidAlgorithmParameterException ex) { |
| // Try another name for NIST P-256 |
| } |
| try { |
| keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME)); |
| return keyPairGenerator.generateKeyPair(); |
| } catch (InvalidAlgorithmParameterException ex) { |
| throw new NoSuchAlgorithmException("Unable to find the NIST P-256 curve", ex); |
| } |
| } |
| |
| /** |
| * Encrypts {@code payload} by using {@code theirPublicKey} and/or {@code sharedSecret}. At |
| * least one of {@code theirPublicKey} and {@code sharedSecret} must be non-null, and an empty |
| * {@code sharedSecret} is equivalent to null. |
| * |
| * <p>Note that {@code header} will be authenticated (but not encrypted) together with {@code |
| * payload}, and the same {@code header} has to be provided for {@link #decrypt}. |
| * |
| * @param theirPublicKey the recipient's public key, or null if the payload is to be encrypted |
| * only with the shared secret |
| * @param sharedSecret the secret shared between the sender and the recipient, or null if the |
| * payload is to be encrypted only with the recipient's public key |
| * @param header the data that will be authenticated with {@code payload} but not encrypted, or |
| * null if the data is empty |
| * @param payload the data to be encrypted, or null if the data is empty |
| * @return the encrypted payload |
| * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported |
| * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms |
| * @hide |
| */ |
| public static byte[] encrypt( |
| @Nullable PublicKey theirPublicKey, |
| @Nullable byte[] sharedSecret, |
| @Nullable byte[] header, |
| @Nullable byte[] payload) |
| throws NoSuchAlgorithmException, InvalidKeyException { |
| sharedSecret = emptyByteArrayIfNull(sharedSecret); |
| if (theirPublicKey == null && sharedSecret.length == 0) { |
| throw new IllegalArgumentException("Both the public key and shared secret are empty"); |
| } |
| header = emptyByteArrayIfNull(header); |
| payload = emptyByteArrayIfNull(payload); |
| |
| KeyPair senderKeyPair; |
| byte[] dhSecret; |
| byte[] hkdfInfo; |
| if (theirPublicKey == null) { |
| senderKeyPair = null; |
| dhSecret = EMPTY_BYTE_ARRAY; |
| hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; |
| } else { |
| senderKeyPair = genKeyPair(); |
| dhSecret = dhComputeSecret(senderKeyPair.getPrivate(), theirPublicKey); |
| hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; |
| } |
| |
| byte[] randNonce = genRandomNonce(); |
| byte[] keyingMaterial = concat(dhSecret, sharedSecret); |
| SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); |
| byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header); |
| if (senderKeyPair == null) { |
| return concat(VERSION, randNonce, ciphertext); |
| } else { |
| return concat( |
| VERSION, encodePublicKey(senderKeyPair.getPublic()), randNonce, ciphertext); |
| } |
| } |
| |
| /** |
| * Decrypts {@code encryptedPayload} by using {@code ourPrivateKey} and/or {@code sharedSecret}. |
| * At least one of {@code ourPrivateKey} and {@code sharedSecret} must be non-null, and an empty |
| * {@code sharedSecret} is equivalent to null. |
| * |
| * <p>Note that {@code header} should be the same data used for {@link #encrypt}, which is |
| * authenticated (but not encrypted) together with {@code payload}; otherwise, an {@code |
| * AEADBadTagException} will be thrown. |
| * |
| * @param ourPrivateKey the recipient's private key, or null if the payload was encrypted only |
| * with the shared secret |
| * @param sharedSecret the secret shared between the sender and the recipient, or null if the |
| * payload was encrypted only with the recipient's public key |
| * @param header the data that was authenticated with the original payload but not encrypted, or |
| * null if the data is empty |
| * @param encryptedPayload the data to be decrypted |
| * @return the original payload that was encrypted |
| * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported |
| * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms |
| * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload} |
| * cannot be validated, or if the payload is not a valid SecureBox V2 payload. |
| * @hide |
| */ |
| public static byte[] decrypt( |
| @Nullable PrivateKey ourPrivateKey, |
| @Nullable byte[] sharedSecret, |
| @Nullable byte[] header, |
| byte[] encryptedPayload) |
| throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { |
| sharedSecret = emptyByteArrayIfNull(sharedSecret); |
| if (ourPrivateKey == null && sharedSecret.length == 0) { |
| throw new IllegalArgumentException("Both the private key and shared secret are empty"); |
| } |
| header = emptyByteArrayIfNull(header); |
| if (encryptedPayload == null) { |
| throw new NullPointerException("Encrypted payload must not be null."); |
| } |
| |
| ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload); |
| byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length); |
| if (!Arrays.equals(version, VERSION)) { |
| throw new AEADBadTagException("The payload was not encrypted by SecureBox v2"); |
| } |
| |
| byte[] senderPublicKeyBytes; |
| byte[] dhSecret; |
| byte[] hkdfInfo; |
| if (ourPrivateKey == null) { |
| dhSecret = EMPTY_BYTE_ARRAY; |
| hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; |
| } else { |
| senderPublicKeyBytes = readEncryptedPayload(ciphertextBuffer, EC_PUBLIC_KEY_LEN_BYTES); |
| dhSecret = dhComputeSecret(ourPrivateKey, decodePublicKey(senderPublicKeyBytes)); |
| hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; |
| } |
| |
| byte[] randNonce = readEncryptedPayload(ciphertextBuffer, GCM_NONCE_LEN_BYTES); |
| byte[] ciphertext = readEncryptedPayload(ciphertextBuffer, ciphertextBuffer.remaining()); |
| byte[] keyingMaterial = concat(dhSecret, sharedSecret); |
| SecretKey decryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); |
| return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header); |
| } |
| |
| private static byte[] readEncryptedPayload(ByteBuffer buffer, int length) |
| throws AEADBadTagException { |
| byte[] output = new byte[length]; |
| try { |
| buffer.get(output); |
| } catch (BufferUnderflowException ex) { |
| throw new AEADBadTagException("The encrypted payload is too short"); |
| } |
| return output; |
| } |
| |
| private static byte[] dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey) |
| throws NoSuchAlgorithmException, InvalidKeyException { |
| KeyAgreement agreement = KeyAgreement.getInstance(KA_ALG); |
| try { |
| agreement.init(ourPrivateKey); |
| } catch (RuntimeException ex) { |
| // Rethrow the RuntimeException as InvalidKeyException |
| throw new InvalidKeyException(ex); |
| } |
| agreement.doPhase(theirPublicKey, /*lastPhase=*/ true); |
| return agreement.generateSecret(); |
| } |
| |
| /** Derives a 128-bit AES key. */ |
| private static SecretKey hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info) |
| throws NoSuchAlgorithmException { |
| Mac mac = Mac.getInstance(MAC_ALG); |
| try { |
| mac.init(new SecretKeySpec(salt, MAC_ALG)); |
| } catch (InvalidKeyException ex) { |
| // This should never happen |
| throw new RuntimeException(ex); |
| } |
| byte[] pseudorandomKey = mac.doFinal(secret); |
| |
| try { |
| mac.init(new SecretKeySpec(pseudorandomKey, MAC_ALG)); |
| } catch (InvalidKeyException ex) { |
| // This should never happen |
| throw new RuntimeException(ex); |
| } |
| mac.update(info); |
| // Hashing just one block will yield 256 bits, which is enough to construct the AES key |
| byte[] hkdfOutput = mac.doFinal(CONSTANT_01); |
| |
| return new SecretKeySpec(Arrays.copyOf(hkdfOutput, GCM_KEY_LEN_BYTES), CIPHER_ALG); |
| } |
| |
| private static byte[] aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad) |
| throws NoSuchAlgorithmException, InvalidKeyException { |
| try { |
| return aesGcmInternal(AesGcmOperation.ENCRYPT, key, nonce, plaintext, aad); |
| } catch (AEADBadTagException ex) { |
| // This should never happen |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| private static byte[] aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad) |
| throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { |
| return aesGcmInternal(AesGcmOperation.DECRYPT, key, nonce, ciphertext, aad); |
| } |
| |
| private static byte[] aesGcmInternal( |
| AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad) |
| throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { |
| Cipher cipher; |
| try { |
| cipher = Cipher.getInstance(ENC_ALG); |
| } catch (NoSuchPaddingException ex) { |
| // This should never happen because AES-GCM doesn't use padding |
| throw new RuntimeException(ex); |
| } |
| GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BYTES * 8, nonce); |
| try { |
| if (operation == AesGcmOperation.DECRYPT) { |
| cipher.init(Cipher.DECRYPT_MODE, key, spec); |
| } else { |
| cipher.init(Cipher.ENCRYPT_MODE, key, spec); |
| } |
| } catch (InvalidAlgorithmParameterException ex) { |
| // This should never happen |
| throw new RuntimeException(ex); |
| } |
| try { |
| cipher.updateAAD(aad); |
| return cipher.doFinal(text); |
| } catch (AEADBadTagException ex) { |
| // Catch and rethrow AEADBadTagException first because it's a subclass of |
| // BadPaddingException |
| throw ex; |
| } catch (IllegalBlockSizeException | BadPaddingException ex) { |
| // This should never happen because AES-GCM can handle inputs of any length without |
| // padding |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| /** |
| * Encodes public key in format expected by the secure hardware module. This is used as part |
| * of the vault params. |
| * |
| * @param publicKey The public key. |
| * @return The key packed into a 65-byte array. |
| */ |
| static byte[] encodePublicKey(PublicKey publicKey) { |
| ECPoint point = ((ECPublicKey) publicKey).getW(); |
| byte[] x = point.getAffineX().toByteArray(); |
| byte[] y = point.getAffineY().toByteArray(); |
| |
| byte[] output = new byte[EC_PUBLIC_KEY_LEN_BYTES]; |
| // The order of arraycopy() is important, because the coordinates may have a one-byte |
| // leading 0 for the sign bit of two's complement form |
| System.arraycopy(y, 0, output, EC_PUBLIC_KEY_LEN_BYTES - y.length, y.length); |
| System.arraycopy(x, 0, output, 1 + EC_COORDINATE_LEN_BYTES - x.length, x.length); |
| output[0] = EC_PUBLIC_KEY_PREFIX; |
| return output; |
| } |
| |
| @VisibleForTesting |
| static PublicKey decodePublicKey(byte[] keyBytes) |
| throws NoSuchAlgorithmException, InvalidKeyException { |
| BigInteger x = |
| new BigInteger( |
| /*signum=*/ 1, |
| Arrays.copyOfRange(keyBytes, 1, 1 + EC_COORDINATE_LEN_BYTES)); |
| BigInteger y = |
| new BigInteger( |
| /*signum=*/ 1, |
| Arrays.copyOfRange( |
| keyBytes, 1 + EC_COORDINATE_LEN_BYTES, EC_PUBLIC_KEY_LEN_BYTES)); |
| |
| // Checks if the point is indeed on the P-256 curve for security considerations |
| validateEcPoint(x, y); |
| |
| KeyFactory keyFactory = KeyFactory.getInstance(EC_ALG); |
| try { |
| return keyFactory.generatePublic(new ECPublicKeySpec(new ECPoint(x, y), EC_PARAM_SPEC)); |
| } catch (InvalidKeySpecException ex) { |
| // This should never happen |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException { |
| if (x.compareTo(EC_PARAM_P) >= 0 |
| || y.compareTo(EC_PARAM_P) >= 0 |
| || x.signum() == -1 |
| || y.signum() == -1) { |
| throw new InvalidKeyException("Point lies outside of the expected curve"); |
| } |
| |
| // Points on the curve satisfy y^2 = x^3 + ax + b (mod p) |
| BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P); |
| BigInteger rhs = |
| x.modPow(BIG_INT_02, EC_PARAM_P) // x^2 |
| .add(EC_PARAM_A) // x^2 + a |
| .mod(EC_PARAM_P) // This will speed up the next multiplication |
| .multiply(x) // (x^2 + a) * x = x^3 + ax |
| .add(EC_PARAM_B) // x^3 + ax + b |
| .mod(EC_PARAM_P); |
| if (!lhs.equals(rhs)) { |
| throw new InvalidKeyException("Point lies outside of the expected curve"); |
| } |
| } |
| |
| private static byte[] genRandomNonce() throws NoSuchAlgorithmException { |
| byte[] nonce = new byte[GCM_NONCE_LEN_BYTES]; |
| new SecureRandom().nextBytes(nonce); |
| return nonce; |
| } |
| |
| @VisibleForTesting |
| static byte[] concat(byte[]... inputs) { |
| int length = 0; |
| for (int i = 0; i < inputs.length; i++) { |
| if (inputs[i] == null) { |
| inputs[i] = EMPTY_BYTE_ARRAY; |
| } |
| length += inputs[i].length; |
| } |
| |
| byte[] output = new byte[length]; |
| int outputPos = 0; |
| for (byte[] input : inputs) { |
| System.arraycopy(input, /*srcPos=*/ 0, output, outputPos, input.length); |
| outputPos += input.length; |
| } |
| return output; |
| } |
| |
| private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) { |
| return input == null ? EMPTY_BYTE_ARRAY : input; |
| } |
| } |