diff options
Diffstat (limited to 'libs/securebox')
-rw-r--r-- | libs/securebox/Android.bp | 8 | ||||
-rw-r--r-- | libs/securebox/OWNERS | 1 | ||||
-rw-r--r-- | libs/securebox/src/com/android/security/SecureBox.java | 461 | ||||
-rw-r--r-- | libs/securebox/tests/Android.bp | 46 | ||||
-rw-r--r-- | libs/securebox/tests/AndroidManifest.xml | 33 | ||||
-rw-r--r-- | libs/securebox/tests/AndroidTest.xml | 31 | ||||
-rw-r--r-- | libs/securebox/tests/src/com/android/security/SecureBoxTest.java | 371 |
7 files changed, 951 insertions, 0 deletions
diff --git a/libs/securebox/Android.bp b/libs/securebox/Android.bp new file mode 100644 index 000000000000..a29c03cfdcca --- /dev/null +++ b/libs/securebox/Android.bp @@ -0,0 +1,8 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "securebox", + srcs: ["src/**/*.java"], +} diff --git a/libs/securebox/OWNERS b/libs/securebox/OWNERS new file mode 100644 index 000000000000..e160799aa10d --- /dev/null +++ b/libs/securebox/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS diff --git a/libs/securebox/src/com/android/security/SecureBox.java b/libs/securebox/src/com/android/security/SecureBox.java new file mode 100644 index 000000000000..0ebaff4ac8e5 --- /dev/null +++ b/libs/securebox/src/com/android/security/SecureBox.java @@ -0,0 +1,461 @@ +/* + * 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. + * + * <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 = + 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. + * + * <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 = 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. + * + * <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 = 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; + } +} diff --git a/libs/securebox/tests/Android.bp b/libs/securebox/tests/Android.bp new file mode 100644 index 000000000000..7df546ae0ff6 --- /dev/null +++ b/libs/securebox/tests/Android.bp @@ -0,0 +1,46 @@ +// Copyright (C) 2022 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "SecureBoxTests", + srcs: [ + "**/*.java", + ], + static_libs: [ + "securebox", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "junit", + "mockito-target-extended-minus-junit4", + "platform-test-annotations", + "testables", + "testng", + "truth-prebuilt", + ], + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], +} diff --git a/libs/securebox/tests/AndroidManifest.xml b/libs/securebox/tests/AndroidManifest.xml new file mode 100644 index 000000000000..3dc956394362 --- /dev/null +++ b/libs/securebox/tests/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.security.tests"> + + <application android:debuggable="true" android:largeHeap="true"> + <uses-library android:name="android.test.mock" /> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Tests for SecureBox" + android:targetPackage="com.android.security.tests"> + </instrumentation> + +</manifest> diff --git a/libs/securebox/tests/AndroidTest.xml b/libs/securebox/tests/AndroidTest.xml new file mode 100644 index 000000000000..54abd13515b4 --- /dev/null +++ b/libs/securebox/tests/AndroidTest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 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. +--> +<configuration description="Runs Tests for SecureBox"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="SecureBoxTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="SecureBoxTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.security.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/libs/securebox/tests/src/com/android/security/SecureBoxTest.java b/libs/securebox/tests/src/com/android/security/SecureBoxTest.java new file mode 100644 index 000000000000..b6e2365038dc --- /dev/null +++ b/libs/securebox/tests/src/com/android/security/SecureBoxTest.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2023 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 static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.expectThrows; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPrivateKeySpec; + +import javax.crypto.AEADBadTagException; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SecureBoxTest { + + private static final int EC_PUBLIC_KEY_LEN_BYTES = 65; + private static final int NUM_TEST_ITERATIONS = 100; + private static final int VERSION_LEN_BYTES = 2; + + // The following fixtures were produced by the C implementation of SecureBox v2. We use these to + // cross-verify the two implementations. + private static final byte[] VAULT_PARAMS = + new byte[] { + (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98, + (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, (byte) 0x94, (byte) 0xfe, + (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07, (byte) 0x8d, (byte) 0x01, + (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, + (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f, + (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d, + (byte) 0x91, (byte) 0x55, (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, + (byte) 0x8b, (byte) 0x32, (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, + (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21, + (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa, (byte) 0x31, + (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x78, (byte) 0x56, (byte) 0x34, (byte) 0x12, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00, (byte) 0x00, + (byte) 0x00 + }; + private static final byte[] VAULT_CHALLENGE = getBytes("Not a real vault challenge"); + private static final byte[] THM_KF_HASH = getBytes("12345678901234567890123456789012"); + private static final byte[] ENCRYPTED_RECOVERY_KEY = + new byte[] { + (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0xe3, (byte) 0xa8, (byte) 0xd0, + (byte) 0x32, (byte) 0x3c, (byte) 0xc7, (byte) 0xe5, (byte) 0xe8, (byte) 0xc1, + (byte) 0x73, (byte) 0x4c, (byte) 0x75, (byte) 0x20, (byte) 0x2e, (byte) 0xb7, + (byte) 0xba, (byte) 0xef, (byte) 0x3e, (byte) 0x3e, (byte) 0xa6, (byte) 0x93, + (byte) 0xe9, (byte) 0xde, (byte) 0xa7, (byte) 0x00, (byte) 0x09, (byte) 0xba, + (byte) 0xa8, (byte) 0x9c, (byte) 0xac, (byte) 0x72, (byte) 0xff, (byte) 0xf6, + (byte) 0x84, (byte) 0x16, (byte) 0xb0, (byte) 0xff, (byte) 0x47, (byte) 0x98, + (byte) 0x53, (byte) 0xc4, (byte) 0xa3, (byte) 0x4a, (byte) 0x54, (byte) 0x21, + (byte) 0x8e, (byte) 0x00, (byte) 0x4b, (byte) 0xfa, (byte) 0xce, (byte) 0xe3, + (byte) 0x79, (byte) 0x8e, (byte) 0x20, (byte) 0x7c, (byte) 0x9b, (byte) 0xc4, + (byte) 0x7c, (byte) 0xd5, (byte) 0x33, (byte) 0x70, (byte) 0x96, (byte) 0xdc, + (byte) 0xa0, (byte) 0x1f, (byte) 0x6e, (byte) 0xbb, (byte) 0x5d, (byte) 0x0c, + (byte) 0x64, (byte) 0x5f, (byte) 0xed, (byte) 0xbf, (byte) 0x79, (byte) 0x8a, + (byte) 0x0e, (byte) 0xd6, (byte) 0x4b, (byte) 0x93, (byte) 0xc9, (byte) 0xcd, + (byte) 0x25, (byte) 0x06, (byte) 0x73, (byte) 0x5e, (byte) 0xdb, (byte) 0xac, + (byte) 0xa8, (byte) 0xeb, (byte) 0x6e, (byte) 0x26, (byte) 0x77, (byte) 0x56, + (byte) 0xd1, (byte) 0x23, (byte) 0x48, (byte) 0xb6, (byte) 0x6a, (byte) 0x15, + (byte) 0xd4, (byte) 0x3e, (byte) 0x38, (byte) 0x7d, (byte) 0x6f, (byte) 0x6f, + (byte) 0x7c, (byte) 0x0b, (byte) 0x93, (byte) 0x4e, (byte) 0xb3, (byte) 0x21, + (byte) 0x44, (byte) 0x86, (byte) 0xf3, (byte) 0x2e + }; + private static final byte[] KEY_CLAIMANT = getBytes("asdfasdfasdfasdf"); + private static final byte[] RECOVERY_CLAIM = + new byte[] { + (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0x16, (byte) 0x75, (byte) 0x5b, + (byte) 0xa2, (byte) 0xdc, (byte) 0x2b, (byte) 0x58, (byte) 0xb9, (byte) 0x66, + (byte) 0xcb, (byte) 0x6f, (byte) 0xb1, (byte) 0xc1, (byte) 0xb0, (byte) 0x1d, + (byte) 0x82, (byte) 0x29, (byte) 0x97, (byte) 0xec, (byte) 0x65, (byte) 0x5e, + (byte) 0xef, (byte) 0x14, (byte) 0xc7, (byte) 0xf0, (byte) 0xf1, (byte) 0x83, + (byte) 0x15, (byte) 0x0b, (byte) 0xcb, (byte) 0x33, (byte) 0x2d, (byte) 0x05, + (byte) 0x20, (byte) 0xdc, (byte) 0xc7, (byte) 0x0d, (byte) 0xc8, (byte) 0xc0, + (byte) 0xc9, (byte) 0xa8, (byte) 0x67, (byte) 0xc8, (byte) 0x16, (byte) 0xfe, + (byte) 0xfb, (byte) 0xb0, (byte) 0x28, (byte) 0x8e, (byte) 0x4f, (byte) 0xd5, + (byte) 0x31, (byte) 0xa7, (byte) 0x94, (byte) 0x33, (byte) 0x23, (byte) 0x15, + (byte) 0x04, (byte) 0xbf, (byte) 0x13, (byte) 0x6a, (byte) 0x28, (byte) 0x8f, + (byte) 0xa6, (byte) 0xfc, (byte) 0x01, (byte) 0xd5, (byte) 0x69, (byte) 0x3d, + (byte) 0x96, (byte) 0x0c, (byte) 0x37, (byte) 0xb4, (byte) 0x1e, (byte) 0x13, + (byte) 0x40, (byte) 0xcc, (byte) 0x44, (byte) 0x19, (byte) 0xf2, (byte) 0xdb, + (byte) 0x49, (byte) 0x80, (byte) 0x9f, (byte) 0xef, (byte) 0xee, (byte) 0x41, + (byte) 0xe6, (byte) 0x3f, (byte) 0xa8, (byte) 0xea, (byte) 0x89, (byte) 0xfe, + (byte) 0x56, (byte) 0x20, (byte) 0xba, (byte) 0x90, (byte) 0x9a, (byte) 0xba, + (byte) 0x0e, (byte) 0x30, (byte) 0xa7, (byte) 0x2b, (byte) 0x0a, (byte) 0x12, + (byte) 0x0b, (byte) 0x03, (byte) 0xd1, (byte) 0x0c, (byte) 0x8e, (byte) 0x82, + (byte) 0x03, (byte) 0xa1, (byte) 0x7f, (byte) 0xc8, (byte) 0xd0, (byte) 0xa9, + (byte) 0x86, (byte) 0x55, (byte) 0x63, (byte) 0xdc, (byte) 0x70, (byte) 0x34, + (byte) 0x21, (byte) 0x2a, (byte) 0x41, (byte) 0x3f, (byte) 0xbb, (byte) 0x82, + (byte) 0x82, (byte) 0xf9, (byte) 0x2b, (byte) 0xd2, (byte) 0x33, (byte) 0x03, + (byte) 0x50, (byte) 0xd2, (byte) 0x27, (byte) 0xeb, (byte) 0x1a + }; + + private static final byte[] TEST_SHARED_SECRET = getBytes("TEST_SHARED_SECRET"); + private static final byte[] TEST_HEADER = getBytes("TEST_HEADER"); + private static final byte[] TEST_PAYLOAD = getBytes("TEST_PAYLOAD"); + + private static final PublicKey THM_PUBLIC_KEY; + private static final PrivateKey THM_PRIVATE_KEY; + + static { + try { + THM_PUBLIC_KEY = + SecureBox.decodePublicKey( + new byte[] { + (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, + (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, + (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c, + (byte) 0x07, (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a, + (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, (byte) 0xd0, + (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, + (byte) 0x3f, (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, + (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55, (byte) 0xb0, + (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32, + (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc, + (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21, + (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa + }); + THM_PRIVATE_KEY = + decodePrivateKey( + new byte[] { + (byte) 0x70, (byte) 0x01, (byte) 0xc7, (byte) 0x87, (byte) 0x32, + (byte) 0x2f, (byte) 0x1c, (byte) 0x9a, (byte) 0x6e, (byte) 0xb1, + (byte) 0x91, (byte) 0xca, (byte) 0x4e, (byte) 0xb5, (byte) 0x44, + (byte) 0xba, (byte) 0xc8, (byte) 0x68, (byte) 0xc6, (byte) 0x0a, + (byte) 0x76, (byte) 0xcb, (byte) 0xd3, (byte) 0x63, (byte) 0x67, + (byte) 0x7c, (byte) 0xb0, (byte) 0x11, (byte) 0x82, (byte) 0x65, + (byte) 0x77, (byte) 0x01 + }); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Test + public void genKeyPair_alwaysReturnsANewKeyPair() throws Exception { + KeyPair keyPair1 = SecureBox.genKeyPair(); + KeyPair keyPair2 = SecureBox.genKeyPair(); + assertThat(keyPair1).isNotEqualTo(keyPair2); + } + + @Test + public void decryptRecoveryClaim() throws Exception { + byte[] claimContent = + SecureBox.decrypt( + THM_PRIVATE_KEY, + /*sharedSecret=*/ null, + ArrayUtils.concat(getBytes("V1 KF_claim"), VAULT_PARAMS, VAULT_CHALLENGE), + RECOVERY_CLAIM); + assertThat(claimContent).isEqualTo(ArrayUtils.concat(THM_KF_HASH, KEY_CLAIMANT)); + } + + @Test + public void decryptRecoveryKey_doesNotThrowForValidAuthenticationTag() throws Exception { + SecureBox.decrypt( + THM_PRIVATE_KEY, + THM_KF_HASH, + ArrayUtils.concat(getBytes("V1 THM_encrypted_recovery_key"), VAULT_PARAMS), + ENCRYPTED_RECOVERY_KEY); + } + + @Test + public void encryptThenDecrypt() throws Exception { + byte[] state = TEST_PAYLOAD; + // Iterate multiple times to amplify any errors + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + state = SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, state); + } + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + state = SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, state); + } + assertThat(state).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullPublicPrivateKeys() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + /*theirPublicKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt( + /*ourPrivateKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullSharedSecret() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, /*sharedSecret=*/ null, TEST_HEADER, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt(THM_PRIVATE_KEY, /*sharedSecret=*/ null, TEST_HEADER, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullHeader() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, TEST_SHARED_SECRET, /*header=*/ null, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, /*header=*/ null, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullPayload() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, /*payload=*/ null); + byte[] decrypted = + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + /*encryptedPayload=*/ encrypted); + assertThat(decrypted.length).isEqualTo(0); + } + + @Test + public void encrypt_nullPublicKeyAndSharedSecret() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.encrypt( + /*theirPublicKey=*/ null, + /*sharedSecret=*/ null, + TEST_HEADER, + TEST_PAYLOAD)); + assertThat(expected.getMessage()).contains("public key and shared secret"); + } + + @Test + public void decrypt_nullPrivateKeyAndSharedSecret() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.decrypt( + /*ourPrivateKey=*/ null, + /*sharedSecret=*/ null, + TEST_HEADER, + TEST_PAYLOAD)); + assertThat(expected.getMessage()).contains("private key and shared secret"); + } + + @Test + public void decrypt_nullEncryptedPayload() throws Exception { + NullPointerException expected = + expectThrows( + NullPointerException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + /*encryptedPayload=*/ null)); + assertThat(expected.getMessage()).contains("payload"); + } + + @Test + public void decrypt_badAuthenticationTag() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + encrypted[encrypted.length - 1] ^= (byte) 1; + + assertThrows( + AEADBadTagException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, encrypted)); + } + + @Test + public void encrypt_invalidPublicKey() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + PublicKey publicKey = keyGen.genKeyPair().getPublic(); + + assertThrows( + InvalidKeyException.class, + () -> SecureBox.encrypt(publicKey, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD)); + } + + @Test + public void decrypt_invalidPrivateKey() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + PrivateKey privateKey = keyGen.genKeyPair().getPrivate(); + + assertThrows( + InvalidKeyException.class, + () -> SecureBox.decrypt(privateKey, TEST_SHARED_SECRET, TEST_HEADER, encrypted)); + } + + @Test + public void decrypt_publicKeyOutsideCurve() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + // Flip the least significant bit of the encoded public key + encrypted[VERSION_LEN_BYTES + EC_PUBLIC_KEY_LEN_BYTES - 1] ^= (byte) 1; + + InvalidKeyException expected = + expectThrows( + InvalidKeyException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + encrypted)); + assertThat(expected.getMessage()).contains("expected curve"); + } + + @Test + public void encodeThenDecodePublicKey() throws Exception { + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + PublicKey originalKey = SecureBox.genKeyPair().getPublic(); + byte[] encodedKey = SecureBox.encodePublicKey(originalKey); + PublicKey decodedKey = SecureBox.decodePublicKey(encodedKey); + assertThat(originalKey).isEqualTo(decodedKey); + } + } + + private static byte[] getBytes(String str) { + return str.getBytes(StandardCharsets.UTF_8); + } + + private static PrivateKey decodePrivateKey(byte[] keyBytes) throws Exception { + assertThat(keyBytes.length).isEqualTo(32); + BigInteger priv = new BigInteger(/*signum=*/ 1, keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(new ECPrivateKeySpec(priv, SecureBox.EC_PARAM_SPEC)); + } +} |