From 0fea4d3d201366ecdca4479fc385fc4df0cdb059 Mon Sep 17 00:00:00 2001 From: juerg Date: Thu, 25 Jan 2024 08:53:01 -0800 Subject: [PATCH] Add InsecureNonceChaCha20Poly1305Jce to aead/internal. This is an implementation of InsecureNonceChaCha20Poly1305 using the ChaCha20Poly1305 provided by the JCE. It doesn't implement the encrypt/decrypt function using ByteBuffer, because it is only intened to be used by HPKE, which doesn't require those functions. PiperOrigin-RevId: 601462223 --- java_src/BUILD.bazel | 2 + .../crypto/tink/aead/internal/BUILD.bazel | 22 ++ .../aead/internal/ChaCha20Poly1305Jce.java | 9 + .../InsecureNonceChaCha20Poly1305Jce.java | 115 ++++++ .../crypto/tink/aead/internal/BUILD.bazel | 20 + .../InsecureNonceChaCha20Poly1305JceTest.java | 357 ++++++++++++++++++ 6 files changed, 525 insertions(+) create mode 100644 java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Jce.java create mode 100644 java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305JceTest.java diff --git a/java_src/BUILD.bazel b/java_src/BUILD.bazel index 805e03d9f4..cbff9c4ed8 100644 --- a/java_src/BUILD.bazel +++ b/java_src/BUILD.bazel @@ -112,6 +112,7 @@ gen_maven_jar_rules( "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_base", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305_base", + "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305_jce", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_x_cha_cha20", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_x_cha_cha20_poly1305", "//src/main/java/com/google/crypto/tink/aead/internal:legacy_full_aead", @@ -565,6 +566,7 @@ gen_maven_jar_rules( "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_base-android", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305-android", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305_base-android", + "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305_jce-android", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_x_cha_cha20-android", "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_x_cha_cha20_poly1305-android", "//src/main/java/com/google/crypto/tink/aead/internal:legacy_full_aead-android", diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel index 9b2e39c3af..ccee65715c 100644 --- a/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel +++ b/java_src/src/main/java/com/google/crypto/tink/aead/internal/BUILD.bazel @@ -383,3 +383,25 @@ android_library( "@maven//:com_google_errorprone_error_prone_annotations", ], ) + +java_library( + name = "insecure_nonce_cha_cha20_poly1305_jce", + srcs = ["InsecureNonceChaCha20Poly1305Jce.java"], + deps = [ + ":cha_cha20_poly1305_jce", + "//src/main/java/com/google/crypto/tink:accesses_partial_key", + "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +android_library( + name = "insecure_nonce_cha_cha20_poly1305_jce-android", + srcs = ["InsecureNonceChaCha20Poly1305Jce.java"], + deps = [ + ":cha_cha20_poly1305_jce-android", + "//src/main/java/com/google/crypto/tink:accesses_partial_key-android", + "//src/main/java/com/google/crypto/tink/config/internal:tink_fips_util-android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/internal/ChaCha20Poly1305Jce.java b/java_src/src/main/java/com/google/crypto/tink/aead/internal/ChaCha20Poly1305Jce.java index 78968b285f..59f4f586a9 100644 --- a/java_src/src/main/java/com/google/crypto/tink/aead/internal/ChaCha20Poly1305Jce.java +++ b/java_src/src/main/java/com/google/crypto/tink/aead/internal/ChaCha20Poly1305Jce.java @@ -129,6 +129,15 @@ public static Aead create(ChaCha20Poly1305Key key) throws GeneralSecurityExcepti key.getOutputPrefix().toByteArray()); } + /** + * Returns a thread-local instance of the ChaCha20Poly1305, or null if ChaCha20Poly1305 is + * not supported. + */ + @Nullable + static Cipher getThreadLocalCipherOrNull() { + return localCipher.get(); + } + public static boolean isSupported() { return localCipher.get() != null; } diff --git a/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Jce.java b/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Jce.java new file mode 100644 index 0000000000..7de5f91d57 --- /dev/null +++ b/java_src/src/main/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305Jce.java @@ -0,0 +1,115 @@ +// Copyright 2021 Google LLC +// +// 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.google.crypto.tink.aead.internal; + +import com.google.crypto.tink.AccessesPartialKey; +import com.google.crypto.tink.config.internal.TinkFipsUtil; +import com.google.errorprone.annotations.Immutable; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.spec.AlgorithmParameterSpec; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Implements ChaCha20Poly1305, as described in RFC 8439, section 2.8. + * + *

It is similar to {@link ChaCha20Poly1305Jce}, but it offers an interface for the user to + * choose the nonce, which is needed in HPKE. + * + *

It uses the JCE, and requires that algorithm "ChaCha20-Poly1305" is present. + */ +@Immutable +public final class InsecureNonceChaCha20Poly1305Jce { + + private static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS = + TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_NOT_FIPS; + + private static final int NONCE_SIZE_IN_BYTES = 12; + private static final int TAG_SIZE_IN_BYTES = 16; + private static final int KEY_SIZE_IN_BYTES = 32; + + private static final String CIPHER_NAME = "ChaCha20-Poly1305"; + private static final String KEY_NAME = "ChaCha20"; + + @SuppressWarnings("Immutable") + private final SecretKey keySpec; + + private InsecureNonceChaCha20Poly1305Jce(final byte[] key) throws GeneralSecurityException { + if (!FIPS.isCompatible()) { + throw new GeneralSecurityException("Can not use ChaCha20Poly1305 in FIPS-mode."); + } + if (!isSupported()) { + throw new GeneralSecurityException("JCE does not support algorithm: " + CIPHER_NAME); + } + if (key.length != KEY_SIZE_IN_BYTES) { + throw new InvalidKeyException("The key length in bytes must be 32."); + } + this.keySpec = new SecretKeySpec(key, KEY_NAME); + } + + @AccessesPartialKey + public static InsecureNonceChaCha20Poly1305Jce create(final byte[] key) + throws GeneralSecurityException { + return new InsecureNonceChaCha20Poly1305Jce(key); + } + + public static boolean isSupported() { + return ChaCha20Poly1305Jce.getThreadLocalCipherOrNull() != null; + } + + public byte[] encrypt(final byte[] nonce, final byte[] plaintext, final byte[] associatedData) + throws GeneralSecurityException { + if (plaintext == null) { + throw new NullPointerException("plaintext is null"); + } + if (nonce.length != NONCE_SIZE_IN_BYTES) { + throw new GeneralSecurityException("nonce length must be " + NONCE_SIZE_IN_BYTES + " bytes."); + } + AlgorithmParameterSpec params = new IvParameterSpec(nonce); + Cipher cipher = ChaCha20Poly1305Jce.getThreadLocalCipherOrNull(); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, params); + if (associatedData != null && associatedData.length != 0) { + cipher.updateAAD(associatedData); + } + return cipher.doFinal(plaintext); + } + + public byte[] decrypt(final byte[] nonce, final byte[] ciphertext, final byte[] associatedData) + throws GeneralSecurityException { + if (ciphertext == null) { + throw new NullPointerException("ciphertext is null"); + } + if (nonce.length != NONCE_SIZE_IN_BYTES) { + throw new GeneralSecurityException("nonce length must be " + NONCE_SIZE_IN_BYTES + " bytes."); + } + if (ciphertext.length < TAG_SIZE_IN_BYTES) { + throw new GeneralSecurityException("ciphertext too short"); + } + AlgorithmParameterSpec params = new IvParameterSpec(nonce); + + Cipher cipher = ChaCha20Poly1305Jce.getThreadLocalCipherOrNull(); + cipher.init(Cipher.DECRYPT_MODE, keySpec, params); + if (associatedData != null && associatedData.length != 0) { + cipher.updateAAD(associatedData); + } + return cipher.doFinal(ciphertext); + } +} diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel index 341fa7a8a1..c4f341870d 100644 --- a/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel +++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/BUILD.bazel @@ -237,3 +237,23 @@ java_test( "@maven//:junit_junit", ], ) + +java_test( + name = "InsecureNonceChaCha20Poly1305JceTest", + size = "small", + srcs = ["InsecureNonceChaCha20Poly1305JceTest.java"], + data = ["@wycheproof//testvectors:all"], + deps = [ + "//src/main/java/com/google/crypto/tink/aead/internal:insecure_nonce_cha_cha20_poly1305_jce", + "//src/main/java/com/google/crypto/tink/config:tink_fips", + "//src/main/java/com/google/crypto/tink/internal:util", + "//src/main/java/com/google/crypto/tink/subtle:bytes", + "//src/main/java/com/google/crypto/tink/subtle:hex", + "//src/main/java/com/google/crypto/tink/subtle:random", + "//src/main/java/com/google/crypto/tink/testing:test_util", + "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util", + "@maven//:com_google_code_gson_gson", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) diff --git a/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305JceTest.java b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305JceTest.java new file mode 100644 index 0000000000..b52caf812f --- /dev/null +++ b/java_src/src/test/java/com/google/crypto/tink/aead/internal/InsecureNonceChaCha20Poly1305JceTest.java @@ -0,0 +1,357 @@ +// Copyright 2021 Google LLC +// +// 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.google.crypto.tink.aead.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.crypto.tink.config.TinkFips; +import com.google.crypto.tink.internal.Util; +import com.google.crypto.tink.subtle.Bytes; +import com.google.crypto.tink.subtle.Hex; +import com.google.crypto.tink.subtle.Random; +import com.google.crypto.tink.testing.TestUtil; +import com.google.crypto.tink.testing.TestUtil.BytesMutation; +import com.google.crypto.tink.testing.WycheproofTestUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.HashSet; +import javax.crypto.AEADBadTagException; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link InsecureNonceChaCha20Poly1305Jce}. */ +@RunWith(JUnit4.class) +public class InsecureNonceChaCha20Poly1305JceTest { + private static final int KEY_SIZE_IN_BYTES = 32; + private static final int NONCE_SIZE_IN_BYTES = 12; + private static final int TAG_SIZE_IN_BYTES = 16; + + public InsecureNonceChaCha20Poly1305Jce createInstance(final byte[] key) + throws GeneralSecurityException { + return InsecureNonceChaCha20Poly1305Jce.create(key); + } + + @Test + public void testSnufflePoly1305ThrowsInvalidAlgorithmParameterExpWhenKeyLenIsGreaterThan32() + throws InvalidKeyException { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + InvalidKeyException e = + assertThrows( + InvalidKeyException.class, + () -> createInstance(new byte[KEY_SIZE_IN_BYTES + 1])); + assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32."); + } + + @Test + public void testSnufflePoly1305ThrowsInvalidAlgorithmParameterExpWhenKeyLenIsLessThan32() + throws InvalidKeyException { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + InvalidKeyException e = + assertThrows( + InvalidKeyException.class, () -> createInstance(new byte[KEY_SIZE_IN_BYTES - 1])); + assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32."); + } + + @Test + public void testDecryptThrowsGeneralSecurityExpWhenCiphertextIsTooShort() + throws GeneralSecurityException { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(new byte[KEY_SIZE_IN_BYTES]); + GeneralSecurityException e = + assertThrows( + GeneralSecurityException.class, + () -> + cipher.decrypt( + new byte[NONCE_SIZE_IN_BYTES], new byte[TAG_SIZE_IN_BYTES - 1], new byte[1])); + assertThat(e).hasMessageThat().containsMatch("ciphertext too short"); + } + + @Test + public void testEncryptDecrypt() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(Random.randBytes(KEY_SIZE_IN_BYTES)); + for (int i = 0; i < 100; i++) { + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + byte[] message = Random.randBytes(i); + byte[] aad = Random.randBytes(i); + byte[] ciphertext = cipher.encrypt(nonce, message, aad); + byte[] decrypted = cipher.decrypt(nonce, ciphertext, aad); + assertArrayEquals(message, decrypted); + } + } + + /** BC had a bug, where GCM failed for messages of size > 8192 */ + @Test + public void testLongMessages() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + Assume.assumeFalse(TestUtil.isAndroid()); // Doesn't work on Android + + int dataSize = 16; + while (dataSize <= (1 << 24)) { + byte[] plaintext = Random.randBytes(dataSize); + byte[] aad = Random.randBytes(dataSize / 3); + byte[] key = Random.randBytes(KEY_SIZE_IN_BYTES); + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(key); + byte[] ciphertext = cipher.encrypt(nonce, plaintext, aad); + byte[] decrypted = cipher.decrypt(nonce, ciphertext, aad); + assertArrayEquals(plaintext, decrypted); + dataSize += 5 * dataSize / 11; + } + } + + @Test + public void testModifyCiphertext() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + byte[] key = Random.randBytes(KEY_SIZE_IN_BYTES); + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(key); + byte[] aad = Random.randBytes(16); + byte[] message = Random.randBytes(32); + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + byte[] ciphertext = cipher.encrypt(nonce, message, aad); + + for (BytesMutation mutation : TestUtil.generateMutations(ciphertext)) { + assertThrows( + String.format( + "Decrypting modified ciphertext should fail : ciphertext = %s, aad = %s," + + " description = %s", + Hex.encode(mutation.value), Arrays.toString(aad), mutation.description), + GeneralSecurityException.class, + () -> { + byte[] unused = cipher.decrypt(nonce, mutation.value, aad); + }); + } + + // Modify AAD + for (int b = 0; b < aad.length; b++) { + for (int bit = 0; bit < 8; bit++) { + byte[] modified = Arrays.copyOf(aad, aad.length); + modified[b] ^= (byte) (1 << bit); + assertThrows( + AEADBadTagException.class, + () -> { + byte[] unused = cipher.decrypt(nonce, ciphertext, modified); + }); + } + } + } + + @Test + public void testNullPlaintextOrCiphertext() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(Random.randBytes(KEY_SIZE_IN_BYTES)); + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + byte[] aad = new byte[] {1, 2, 3}; + assertThrows( + NullPointerException.class, + () -> { + byte[] unused = cipher.encrypt(nonce, null, aad); + }); + assertThrows( + NullPointerException.class, + () -> { + byte[] unused = cipher.encrypt(nonce, null, null); + }); + assertThrows( + NullPointerException.class, + () -> { + byte[] unused = cipher.decrypt(nonce, null, aad); + }); + assertThrows( + NullPointerException.class, + () -> { + byte[] unused = cipher.decrypt(nonce, null, null); + }); + } + + @Test + public void testEmptyAssociatedData() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + byte[] aad = new byte[0]; + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(Random.randBytes(KEY_SIZE_IN_BYTES)); + for (int messageSize = 0; messageSize < 75; messageSize++) { + byte[] message = Random.randBytes(messageSize); + { // encrypting with aad as a 0-length array + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + byte[] ciphertext = cipher.encrypt(nonce, message, aad); + byte[] decrypted = cipher.decrypt(nonce, ciphertext, aad); + assertArrayEquals(message, decrypted); + byte[] decrypted2 = cipher.decrypt(nonce, ciphertext, null); + assertArrayEquals(message, decrypted2); + byte[] badAad = new byte[] {1, 2, 3}; + assertThrows( + AEADBadTagException.class, + () -> { + byte[] unused = cipher.decrypt(nonce, ciphertext, badAad); + }); + } + { // encrypting with aad equal to null + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + byte[] ciphertext = cipher.encrypt(nonce, message, null); + byte[] decrypted = cipher.decrypt(nonce, ciphertext, aad); + assertArrayEquals(message, decrypted); + byte[] decrypted2 = cipher.decrypt(nonce, ciphertext, null); + assertArrayEquals(message, decrypted2); + byte[] badAad = new byte[] {1, 2, 3}; + assertThrows( + AEADBadTagException.class, + () -> { + byte[] unused = cipher.decrypt(nonce, ciphertext, badAad); + }); + } + } + } + + /** + * This test simply checks that multiple ciphertexts of the same message with a different nonce + * are distinct. + */ + @Test + public void testNonce() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + byte[] key = Random.randBytes(KEY_SIZE_IN_BYTES); + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(key); + byte[] message = new byte[0]; + byte[] aad = new byte[0]; + HashSet ciphertexts = new HashSet<>(); + final int samples = 1 << 10; + for (int i = 0; i < samples; i++) { + byte[] nonce = Random.randBytes(NONCE_SIZE_IN_BYTES); + byte[] ct = cipher.encrypt(nonce, message, aad); + String ctHex = Hex.encode(ct); + assertThat(ciphertexts).doesNotContain(ctHex); + ciphertexts.add(ctHex); + } + assertThat(ciphertexts).hasSize(samples); + } + + @Test + public void testWycheproofVectors() throws Exception { + Assume.assumeFalse(TinkFips.useOnlyFips()); + Assume.assumeTrue(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + JsonObject json = + WycheproofTestUtil.readJson( + "../wycheproof/testvectors/chacha20_poly1305_test.json"); + int errors = 0; + JsonArray testGroups = json.getAsJsonArray("testGroups"); + for (int i = 0; i < testGroups.size(); i++) { + JsonObject group = testGroups.get(i).getAsJsonObject(); + JsonArray tests = group.getAsJsonArray("tests"); + for (int j = 0; j < tests.size(); j++) { + JsonObject testcase = tests.get(j).getAsJsonObject(); + String tcId = + String.format( + "testcase %d (%s)", + testcase.get("tcId").getAsInt(), testcase.get("comment").getAsString()); + byte[] iv = Hex.decode(testcase.get("iv").getAsString()); + byte[] key = Hex.decode(testcase.get("key").getAsString()); + byte[] msg = Hex.decode(testcase.get("msg").getAsString()); + byte[] aad = Hex.decode(testcase.get("aad").getAsString()); + byte[] ct = Hex.decode(testcase.get("ct").getAsString()); + byte[] tag = Hex.decode(testcase.get("tag").getAsString()); + byte[] ciphertext = Bytes.concat(ct, tag); + // Result is one of "valid", "invalid", "acceptable". + // "valid" are test vectors with matching plaintext, ciphertext and tag. + // "invalid" are test vectors with invalid parameters or invalid ciphertext and tag. + // "acceptable" are test vectors with weak parameters or legacy formats. + String result = testcase.get("result").getAsString(); + try { + InsecureNonceChaCha20Poly1305Jce cipher = createInstance(key); + + // Some test-cases use the same IV, which makes the JDK implementation of this cipher + // fail, because it detects IV re-use. + // To prevent this we first call encrypt with a random iv. + byte[] unused = cipher.encrypt(Random.randBytes(NONCE_SIZE_IN_BYTES), msg, aad); + + // Encryption. + byte[] encrypted = cipher.encrypt(iv, msg, aad); + boolean ciphertextMatches = TestUtil.arrayEquals(encrypted, ciphertext); + if (result.equals("valid") && !ciphertextMatches) { + System.err.printf( + "FAIL %s: incorrect encryption, result: %s, expected: %s%n", + tcId, Hex.encode(encrypted), Hex.encode(ciphertext)); + errors++; + } + // Decryption. + byte[] decrypted = cipher.decrypt(iv, ciphertext, aad); + boolean plaintextMatches = TestUtil.arrayEquals(decrypted, msg); + if (result.equals("invalid")) { + System.out.printf( + "FAIL %s: accepting invalid ciphertext, cleartext: %s, decrypted: %s%n", + tcId, Hex.encode(msg), Hex.encode(decrypted)); + errors++; + } else { + if (!plaintextMatches) { + System.out.printf( + "FAIL %s: incorrect decryption, result: %s, expected: %s%n", + tcId, Hex.encode(decrypted), Hex.encode(msg)); + errors++; + } + } + } catch (GeneralSecurityException ex) { + if (result.equals("valid")) { + System.out.printf("FAIL %s: cannot decrypt, exception %s%n", tcId, ex); + errors++; + } + } + } + } + assertEquals(0, errors); + } + + @Test + public void testIsSupportedOnNewerAndroidVersions() throws Exception { + Assume.assumeTrue(TestUtil.isAndroid()); + Integer androidApiLevel = Util.getAndroidApiLevel(); + assertThat(InsecureNonceChaCha20Poly1305Jce.isSupported()).isEqualTo(androidApiLevel >= 29); + } + + @Test + public void testCreateFailsIfNotSupported() throws Exception { + Assume.assumeFalse(InsecureNonceChaCha20Poly1305Jce.isSupported()); + + byte[] key = Random.randBytes(32); + assertThrows( + GeneralSecurityException.class, () -> InsecureNonceChaCha20Poly1305Jce.create(key)); + } +}