Pull SecureBox.java out into its own library.
SecureBox is needed by Settings for encrypting device credential and
was inaccessible from services/core/java/com/android/server.
Create a new SecureBox library to resolve.
Test: atest com.android.server.locksettings.recoverablekeystore
SecureBoxTests
Bug: 258505917
Change-Id: I65484edf12b04dfe1642cd0c97bc999d26430395
diff --git a/libs/securebox/Android.bp b/libs/securebox/Android.bp
new file mode 100644
index 0000000..a29c03c
--- /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 0000000..e160799
--- /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 0000000..0ebaff4
--- /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 0000000..7df546a
--- /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 0000000..3dc9563
--- /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 0000000..54abd135
--- /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 0000000..b6e2365
--- /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));
+ }
+}