/* * 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.security; import android.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; 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. * *

Securebox v2 provides a simple interface to perform encryptions by using any of the following * credential types: * *

* * @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 = ArrayUtils.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. * *

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 = ArrayUtils.concat(dhSecret, sharedSecret); SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header); if (senderKeyPair == null) { return ArrayUtils.concat(VERSION, randNonce, ciphertext); } else { return ArrayUtils.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. * *

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 = ArrayUtils.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. */ public 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; } /** * Decodes byte[] encoded public key. * * @param keyBytes encoded public key * @return the public key */ public 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; } private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) { return input == null ? EMPTY_BYTE_ARRAY : input; } }