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