Home | History | Annotate | Download | only in recoverablekeystore
      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 
    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      */
    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      */
    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      */
    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 
    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 
    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. */
    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 
    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 
    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 
    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      */
    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
    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 
    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 
    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
    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 
    468     private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) {
    469         return input == null ? EMPTY_BYTE_ARRAY : input;
    470     }
    471 }
    472