aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-03-10 02:06:55 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-03-10 02:06:55 +0000
commitc9d617b97a62bd58094494bb79cb495bfebb498e (patch)
tree474ef4db5c4116e813bcb88edf7328ef3f34869c
parent390b8ecab803803bbb5a7d64503a06c340aab579 (diff)
parent8f67dd5634047aa5a50c9ed9aed90fff39cd5982 (diff)
downloadwycheproof-c9d617b97a62bd58094494bb79cb495bfebb498e.tar.gz
Snap for 8283941 from 8f67dd5634047aa5a50c9ed9aed90fff39cd5982 to tm-release
Change-Id: I46d55cfbcb741ebf59c2cac4ca149de53ae72bdb
-rw-r--r--Android.bp14
-rw-r--r--keystore-cts-src/OWNERS9
-rw-r--r--keystore-cts-src/android/cts/keystore/wycheproof/CertificateUtil.java83
-rw-r--r--keystore-cts-src/android/cts/keystore/wycheproof/TestUtil.java69
-rw-r--r--keystore-cts-src/android/cts/keystore/wycheproof/testcases/AesGcmTest.java984
5 files changed, 1159 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp
index f379214..7692b25 100644
--- a/Android.bp
+++ b/Android.bp
@@ -50,3 +50,17 @@ java_library_static {
"junit",
],
}
+
+java_library_static {
+ name: "wycheproof-keystore",
+ visibility: [
+ "//cts/tests/tests/keystore",
+ ],
+ srcs: ["keystore-cts-src/**/*.java"],
+ sdk_version: "current",
+ libs: [
+ "bouncycastle-unbundled",
+ "bouncycastle-bcpkix-unbundled",
+ "junit",
+ ],
+}
diff --git a/keystore-cts-src/OWNERS b/keystore-cts-src/OWNERS
new file mode 100644
index 0000000..aad2496
--- /dev/null
+++ b/keystore-cts-src/OWNERS
@@ -0,0 +1,9 @@
+# Bug Component 1084732
+
+# EMEA, Primary reviewers
+eranm@google.com
+drysdale@google.com
+
+# US
+jdanis@google.com
+sethmo@google.com
diff --git a/keystore-cts-src/android/cts/keystore/wycheproof/CertificateUtil.java b/keystore-cts-src/android/cts/keystore/wycheproof/CertificateUtil.java
new file mode 100644
index 0000000..4a12c75
--- /dev/null
+++ b/keystore-cts-src/android/cts/keystore/wycheproof/CertificateUtil.java
@@ -0,0 +1,83 @@
+/**
+ * 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 android.keystore.cts.wycheproof;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import javax.security.auth.x500.X500Principal;
+
+/** Certificate utilities */
+public class CertificateUtil {
+
+ public static X509Certificate createCertificate(
+ KeyPair keyPair, X500Principal subject, X500Principal issuer)
+ throws OperatorCreationException, CertificateException, IOException {
+ // Make the certificate valid for two days.
+ long millisPerDay = 24 * 60 * 60 * 1000;
+ long now = System.currentTimeMillis();
+ Date start = new Date(now - millisPerDay);
+ Date end = new Date(now + millisPerDay);
+
+ // Assign a random serial number.
+ byte[] serialBytes = new byte[16];
+ new SecureRandom().nextBytes(serialBytes);
+ BigInteger serialNumber = new BigInteger(1, serialBytes);
+
+ // Create the certificate builder
+ X509v3CertificateBuilder x509cg =
+ new X509v3CertificateBuilder(
+ X500Name.getInstance(issuer.getEncoded()),
+ serialNumber,
+ start,
+ end,
+ X500Name.getInstance(subject.getEncoded()),
+ SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
+
+ // Choose a signature algorithm matching the key format.
+ String keyAlgorithm = keyPair.getPrivate().getAlgorithm();
+ String signatureAlgorithm;
+ if (keyAlgorithm.equals("RSA")) {
+ signatureAlgorithm = "SHA256withRSA";
+ } else if (keyAlgorithm.equals("EC")) {
+ signatureAlgorithm = "SHA256withECDSA";
+ } else {
+ throw new IllegalArgumentException("Unknown key algorithm " + keyAlgorithm);
+ }
+
+ // Sign the certificate and generate it.
+ X509CertificateHolder x509holder =
+ x509cg.build(
+ new JcaContentSignerBuilder(signatureAlgorithm)
+ .build(keyPair.getPrivate()));
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ X509Certificate x509c =
+ (X509Certificate)
+ certFactory.generateCertificate(
+ new ByteArrayInputStream(x509holder.getEncoded()));
+ return x509c;
+ }
+}
diff --git a/keystore-cts-src/android/cts/keystore/wycheproof/TestUtil.java b/keystore-cts-src/android/cts/keystore/wycheproof/TestUtil.java
new file mode 100644
index 0000000..a8ce77d
--- /dev/null
+++ b/keystore-cts-src/android/cts/keystore/wycheproof/TestUtil.java
@@ -0,0 +1,69 @@
+/**
+ * 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 android.keystore.cts.wycheproof;
+
+import java.nio.ByteBuffer;
+import java.security.Provider;
+import java.security.Security;
+
+/** Test utilities */
+public class TestUtil {
+
+ public static String bytesToHex(byte[] bytes) {
+ // bytesToHex is used to convert output from Cipher.
+ // cipher.update can return null, which is equivalent to returning
+ // no plaitext rsp. ciphertext.
+ if (bytes == null) {
+ return "";
+ }
+ String chars = "0123456789abcdef";
+ StringBuilder result = new StringBuilder(2 * bytes.length);
+ for (byte b : bytes) {
+ // convert to unsigned
+ int val = b & 0xff;
+ result.append(chars.charAt(val / 16));
+ result.append(chars.charAt(val % 16));
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a hexadecimal representation of the bytes written to ByteBuffer (i.e. all the bytes
+ * before position()).
+ */
+ public static String byteBufferToHex(ByteBuffer buffer) {
+ ByteBuffer tmp = buffer.duplicate();
+ tmp.flip();
+ byte[] bytes = new byte[tmp.remaining()];
+ tmp.get(bytes);
+ return bytesToHex(bytes);
+ }
+
+ public static byte[] hexToBytes(String hex) throws IllegalArgumentException {
+ if (hex.length() % 2 != 0) {
+ throw new IllegalArgumentException("Expected a string of even length");
+ }
+ int size = hex.length() / 2;
+ byte[] result = new byte[size];
+ for (int i = 0; i < size; i++) {
+ int hi = Character.digit(hex.charAt(2 * i), 16);
+ int lo = Character.digit(hex.charAt(2 * i + 1), 16);
+ if ((hi == -1) || (lo == -1)) {
+ throw new IllegalArgumentException("input is not hexadecimal");
+ }
+ result[i] = (byte) (16 * hi + lo);
+ }
+ return result;
+ }
+}
diff --git a/keystore-cts-src/android/cts/keystore/wycheproof/testcases/AesGcmTest.java b/keystore-cts-src/android/cts/keystore/wycheproof/testcases/AesGcmTest.java
new file mode 100644
index 0000000..c1b31c3
--- /dev/null
+++ b/keystore-cts-src/android/cts/keystore/wycheproof/testcases/AesGcmTest.java
@@ -0,0 +1,984 @@
+/**
+ * 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 android.keystore.cts.wycheproof;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import java.nio.ByteBuffer;
+import java.security.AlgorithmParameterGenerator;
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.junit.Test;
+import org.junit.Ignore;
+import org.junit.Before;
+import android.security.keystore.KeyProtection;
+import android.security.keystore.KeyProperties;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.UnrecoverableKeyException;
+
+// TODO(bleichen):
+// - For EAX I was able to derive some special cases by inverting OMAC.
+// Not sure if that is possible here.
+/**
+ * Testing AES-GCM
+ *
+ * <p>Other tests using AES-GCM are: CipherInputStreamTest.java CipherOutputStreamTest.java
+ */
+public class AesGcmTest {
+ private static final String EXPECTED_PROVIDER_NAME = "AndroidKeyStoreBCWorkaround";
+ private KeyStore keyStore;
+
+ @Before
+ public void setup() throws Exception {
+ keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ for (GcmTestVector test : GCM_TEST_VECTORS) {
+ setKeystoreEntry(test.alias, test.key);
+ }
+ }
+
+ private SecretKey setKeystoreEntry(String alias, SecretKeySpec key)
+ throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+ keyStore.setEntry(
+ alias,
+ new KeyStore.SecretKeyEntry(key),
+ new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_ECB, KeyProperties.BLOCK_MODE_GCM)
+ .setRandomizedEncryptionRequired(false)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .build());
+ // Key imported, obtain a reference to it.
+ return (SecretKey) keyStore.getKey(alias, null);
+ }
+
+ /** Test vectors */
+ public static class GcmTestVector {
+ final byte[] pt;
+ final byte[] aad;
+ final byte[] ct;
+ final String ptHex;
+ final String ctHex;
+ final GCMParameterSpec parameters;
+ final SecretKeySpec key;
+ final int nonceLengthInBits;
+ final int tagLengthInBits;
+ final String alias;
+
+ public GcmTestVector(
+ String message,
+ String keyMaterial,
+ String nonce,
+ String aad,
+ String ciphertext,
+ String tag,
+ String alias) {
+ this.ptHex = message;
+ this.pt = TestUtil.hexToBytes(message);
+ this.aad = TestUtil.hexToBytes(aad);
+ this.ct = TestUtil.hexToBytes(ciphertext + tag);
+ this.ctHex = ciphertext + tag;
+ this.tagLengthInBits = 4 * tag.length();
+ this.nonceLengthInBits = 4 * nonce.length();
+ this.parameters = new GCMParameterSpec(tagLengthInBits, TestUtil.hexToBytes(nonce));
+ this.key = new SecretKeySpec(TestUtil.hexToBytes(keyMaterial), "AES");
+ this.alias = alias;
+ }
+ };
+
+ private static final GcmTestVector[] GCM_TEST_VECTORS = {
+ new GcmTestVector(
+ "001d0c231287c1182784554ca3a21908",
+ "5b9604fe14eadba931b0ccf34843dab9",
+ "028318abc1824029138141a2",
+ "",
+ "26073cc1d851beff176384dc9896d5ff",
+ "0a3ea7a5487cb5f7d70fb6c58d038554", "Key1"),
+ new GcmTestVector(
+ "001d0c231287c1182784554ca3a21908",
+ "5b9604fe14eadba931b0ccf34843dab9",
+ "921d2507fa8007b7bd067d34",
+ "00112233445566778899aabbccddeeff",
+ "49d8b9783e911913d87094d1f63cc765",
+ "1e348ba07cca2cf04c618cb4", "Key2"),
+ new GcmTestVector(
+ "2035af313d1346ab00154fea78322105",
+ "aa023d0478dcb2b2312498293d9a9129",
+ "0432bc49ac34412081288127",
+ "aac39231129872a2",
+ "eea945f3d0f98cc0fbab472a0cf24e87",
+ "4bb9b4812519dadf9e1232016d068133", "Key3"),
+ new GcmTestVector(
+ "2035af313d1346ab00154fea78322105",
+ "aa023d0478dcb2b2312498293d9a9129",
+ "0432bc49ac344120",
+ "aac39231129872a2",
+ "64c36bb3b732034e3a7d04efc5197785",
+ "b7d0dd70b00d65b97cfd080ff4b819d1", "Key4"),
+ new GcmTestVector(
+ "02efd2e5782312827ed5d230189a2a342b277ce048462193",
+ "2034a82547276c83dd3212a813572bce",
+ "3254202d854734812398127a3d134421",
+ "1a0293d8f90219058902139013908190bc490890d3ff12a3",
+ "64069c2d58690561f27ee199e6b479b6369eec688672bde9",
+ "9b7abadd6e69c1d9ec925786534f5075", "Key5"),
+ // GCM uses GHASH to compute the initial counter J0 if the nonce is not 12 bytes long.
+ // The counter is incremented modulo 2^32 in counter mode. The following test vectors verify
+ // the behavior of an implementation for initial counter values J0 close to a 2^32 limit.
+ // J0:00000000000000000000000000000000
+ new GcmTestVector(
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "00112233445566778899aabbccddeeff",
+ "7b95b8c356810a84711d68150a1b7750",
+ "",
+ "84d4c9c08b4f482861e3a9c6c35bc4d91df927374513bfd49f436bd73f325285daef4ff7e13d46a6",
+ "213a3cb93855d18e69337eee66aeec07", "Key6"),
+ // J0:ffffffffffffffffffffffffffffffff
+ new GcmTestVector(
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "00112233445566778899aabbccddeeff",
+ "1a552e67cdc4dc1a33b824874ebf0bed",
+ "",
+ "948ca37a8e6649e88aeffb1c598f3607007702417ea0e0bc3c60ad5a949886de968cf53ea6462aed",
+ "99b381bfa2af9751c39d1b6e86d1be6a", "Key6"),
+ // J0:000102030405060708090a0bffffffff
+ new GcmTestVector(
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "00112233445566778899aabbccddeeff",
+ "99821c2dd5daecded07300f577f7aff1",
+ "",
+ "127af9b39ecdfc57bb11a2847c7c2d3d8f938f40f877e0c4af37d0fe9af033052bd537c4ae978f60",
+ "07eb2fe4a958f8434d40684899507c7c", "Key7"),
+ // J0:000102030405060708090a0bfffffffe
+ new GcmTestVector(
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "00112233445566778899aabbccddeeff",
+ "5e4a3900142358d1c774d8d124d8d27d",
+ "",
+ "0cf6ae47156b14dce03c8a07a2e172b1127af9b39ecdfc57bb11a2847c7c2d3d8f938f40f877e0c4",
+ "f145c2dcaf339eede427be934357eac0", "Key8"),
+ };
+
+ /**
+ * Returns the GCM test vectors supported by the current provider. This is necessary since not
+ * every provider supports all parameters sizes. For example SUNJCE does not support 8 byte tags
+ * and Conscrypt only supports 12 byte nonces. Such restrictions are often made because AES-GCM is
+ * a relatively weak algorithm and especially small parameter sizes can lead to easy attacks.
+ * Avoiding such small parameter sizes should not be seen as a bug in the library.
+ *
+ * <p>The only assumption we make here is that all test vectors with 128 bit tags and nonces with
+ * at least 96 bits are supported.
+ */
+ private Iterable<GcmTestVector> getTestVectors() throws Exception {
+ ArrayList<GcmTestVector> supported = new ArrayList<GcmTestVector>();
+ for (GcmTestVector test : GCM_TEST_VECTORS) {
+ if (test.nonceLengthInBits != 96 || test.tagLengthInBits != 128) {
+ try {
+ // Checks whether the parameter size is supported.
+ // It would be nice if there was a way to check this without trying to encrypt.
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException ex) {
+ // Not supported
+ continue;
+ }
+ }
+ supported.add(test);
+ }
+ return supported;
+ }
+
+ @Test
+ public void testVectors() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ byte[] ct = cipher.doFinal(test.pt);
+ assertEquals(test.ctHex, TestUtil.bytesToHex(ct));
+ }
+ }
+
+ /** Test encryption when update and doFinal are done with empty byte arrays. */
+ @Test
+ public void testEncryptWithEmptyArrays() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ byte[] empty = new byte[0];
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.pt.length);
+ ByteBuffer ctBuffer = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(empty);
+ cipher.updateAAD(test.aad);
+ byte[] res = cipher.update(empty);
+ if (res != null) {
+ ctBuffer.put(res);
+ }
+ res = cipher.update(test.pt);
+ if (res != null) {
+ ctBuffer.put(res);
+ }
+ res = cipher.doFinal(empty);
+ if (res != null) {
+ ctBuffer.put(res);
+ }
+ assertEquals(test.ctHex, TestUtil.byteBufferToHex(ctBuffer));
+ }
+ }
+
+ @Test
+ public void testDecryptWithEmptyArrays() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ byte[] empty = new byte[0];
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.ct.length);
+ ByteBuffer ptBuffer = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(empty);
+ cipher.updateAAD(test.aad);
+ byte[] res = cipher.update(empty);
+ if (res != null) {
+ ptBuffer.put(res);
+ }
+ res = cipher.update(test.ct);
+ if (res != null) {
+ ptBuffer.put(res);
+ }
+ res = cipher.doFinal(empty);
+ if (res != null) {
+ ptBuffer.put(res);
+ }
+ assertEquals(test.ptHex, TestUtil.byteBufferToHex(ptBuffer));
+
+ // Simple test that a modified ciphertext fails.
+ ptBuffer.clear();
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(empty);
+ cipher.updateAAD(test.aad);
+ cipher.updateAAD(new byte[1]);
+ res = cipher.update(empty);
+ if (res != null) {
+ ptBuffer.put(res);
+ }
+ res = cipher.update(test.ct);
+ if (res != null) {
+ ptBuffer.put(res);
+ }
+ try {
+ cipher.doFinal(empty);
+ fail("Accepted modified ciphertext.");
+ } catch (GeneralSecurityException ex) {
+ // Expected
+ }
+ }
+ }
+
+ /**
+ * Typically one should always call updateAAD before any call to update. This test checks what
+ * happens if the order is reversed. The test expects that a correct implementation either
+ * computes the tag correctly or throws an exception.
+ *
+ * <p>For example, OpenJdk did compute incorrect tags in this case. The bug has been fixed in
+ * http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/89c06ca1e6cc
+ *
+ * <p>For example BouncyCastle computes correct tags if the calls are reversed, SunJCE and OpenJdk
+ * now throw exceptions.
+ */
+ @Test
+ public void testLateUpdateAAD() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ byte[] c0 = cipher.update(test.pt);
+ try {
+ cipher.updateAAD(test.aad);
+ } catch (java.lang.IllegalStateException ex) {
+ // Throwing an exception is valid behaviour.
+ continue;
+ }
+ byte[] c1 = cipher.doFinal();
+ String result = TestUtil.bytesToHex(c0) + TestUtil.bytesToHex(c1);
+ assertEquals(test.ctHex, result);
+ }
+ }
+
+ /**
+ * JCE has a dangerous feature: after a doFinal the cipher is typically reinitialized using the
+ * previous IV. This "feature" can easily break AES-GCM usages, because encrypting twice with the
+ * same key and IV leaks the authentication key. Hence any reasonable implementation of AES-GCM
+ * should not allow this. The expected behaviour of OpenJDK can be derived from the tests in
+ * jdk/test/com/sun/crypto/provider/Cipher/AES/TestGCMKeyAndIvCheck.java. OpenJDK does not allow
+ * two consecutive initializations for encryption with the same key and IV.
+ *
+ * <p>The test here is weaker than the restrictions in OpenJDK. The only requirement here is that
+ * reusing a Cipher without an explicit init() is caught.
+ *
+ * <p>BouncyCastle 1.52 failed this test
+ *
+ * <p>Conscrypt failed this test
+ */
+ @Test
+ public void testIvReuse() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ byte[] ct1 = cipher.doFinal(test.pt);
+ try {
+ byte[] ct2 = cipher.doFinal(test.pt);
+ fail(
+ "It should not possible to reuse an IV."
+ + " ct1:"
+ + TestUtil.bytesToHex(ct1)
+ + " ct2:"
+ + TestUtil.bytesToHex(ct2));
+ } catch (java.lang.IllegalStateException ex) {
+ // This is expected.
+ }
+ }
+ }
+
+ /**
+ * Checks whether the implementation requires larger ByteBuffers than necessary. This test has
+ * been added mostly for debugging. E.g., conscrypt failed during decryption with ByteBuffers
+ * simply because the necessary outputSize was computed incorrectly.
+ */
+ @Test
+ public void testByteBufferSize() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ // Encryption
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.pt.length);
+ assertEquals("plaintext size:" + test.pt.length, test.ct.length, outputSize);
+ // Decryption
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ outputSize = cipher.getOutputSize(test.ct.length);
+ assertEquals("ciphertext size:" + test.ct.length, test.pt.length, outputSize);
+ }
+ }
+
+ /** Encryption with ByteBuffers. */
+ @Test
+ public void testByteBuffer() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ ByteBuffer ptBuffer = ByteBuffer.wrap(test.pt);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.pt.length);
+ ByteBuffer ctBuffer = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ptBuffer, ctBuffer);
+ assertEquals(test.ctHex, TestUtil.byteBufferToHex(ctBuffer));
+
+ // Decryption
+ ctBuffer.flip();
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ outputSize = cipher.getOutputSize(test.ct.length);
+ ByteBuffer decrypted = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ctBuffer, decrypted);
+ assertEquals(test.ptHex, TestUtil.byteBufferToHex(decrypted));
+ }
+ }
+
+ /** Encryption with ByteBuffers should be copy-safe. */
+ @Test
+ public void testByteBufferAlias() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.pt.length);
+ byte[] backingArray = new byte[outputSize];
+ ByteBuffer ptBuffer = ByteBuffer.wrap(backingArray);
+ ptBuffer.put(test.pt);
+ ptBuffer.flip();
+ ByteBuffer ctBuffer = ByteBuffer.wrap(backingArray);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ptBuffer, ctBuffer);
+ assertEquals(test.ctHex, TestUtil.byteBufferToHex(ctBuffer));
+
+ // Decryption
+ ByteBuffer decrypted = ByteBuffer.wrap(backingArray);
+ ctBuffer.flip();
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ctBuffer, decrypted);
+ assertEquals(test.ptHex, TestUtil.byteBufferToHex(decrypted));
+ }
+ }
+
+ /** Encryption and decryption with large arrays should be copy-safe. */
+ @Test
+ public void testLargeArrayAlias() throws Exception {
+ byte[] ptVector = new byte[8192];
+
+ // this offset is relative to the start of the input, not the start of the buffer.
+ for (int outputOffset = -32; outputOffset <= 32; outputOffset++) {
+ // try with doFinal directly as well as with update followed by doFinal
+ for (int useUpdate = 0; useUpdate <= 1; useUpdate++) {
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(new byte[16], "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ GCMParameterSpec parameters = new GCMParameterSpec(128, new byte[12]);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameters);
+
+ // these offsets are relative to the start of the buffer
+ int inputOffsetInBuffer = 32;
+ int outputOffsetInBuffer = inputOffsetInBuffer + outputOffset;
+ int sliceLength = cipher.getOutputSize(ptVector.length);
+
+ byte[] inBuf = new byte[sliceLength + Math.max(inputOffsetInBuffer, outputOffsetInBuffer)];
+ byte[] outBuf = inBuf;
+
+ System.arraycopy(ptVector, 0, inBuf, inputOffsetInBuffer, ptVector.length);
+
+ try {
+ int ctLength = 0;
+ if (useUpdate > 0) {
+ ctLength +=
+ cipher.update(
+ inBuf, inputOffsetInBuffer, ptVector.length, outBuf, outputOffsetInBuffer);
+ ctLength += cipher.doFinal(inBuf, 0, 0, outBuf, outputOffsetInBuffer + ctLength);
+ } else {
+ ctLength +=
+ cipher.doFinal(
+ inBuf, inputOffsetInBuffer, ptVector.length, outBuf, outputOffsetInBuffer);
+ }
+
+ System.arraycopy(outBuf, outputOffsetInBuffer, inBuf, inputOffsetInBuffer, ctLength);
+
+ cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, parameters);
+
+ int resultPtLength = 0;
+ if (useUpdate > 0) {
+ resultPtLength +=
+ cipher.update(inBuf, inputOffsetInBuffer, ctLength, outBuf, outputOffsetInBuffer);
+ resultPtLength +=
+ cipher.doFinal(inBuf, 0, 0, outBuf, outputOffsetInBuffer + resultPtLength);
+ } else {
+ resultPtLength +=
+ cipher.doFinal(inBuf, inputOffsetInBuffer, ctLength, outBuf, outputOffsetInBuffer);
+ }
+
+ assertEquals(resultPtLength, ptVector.length);
+ assertArrayEquals(
+ ptVector,
+ Arrays.copyOfRange(
+ outBuf, outputOffsetInBuffer, outputOffsetInBuffer + resultPtLength));
+ } catch (Throwable t) {
+ throw new AssertionError(
+ "testLargeByteBufferAlias failed with outputOffset=" + outputOffset, t);
+ }
+ }
+ }
+ }
+
+ /**
+ * Encryption with ByteBuffers should be copy-safe even if the buffers have different starting
+ * offsets and/or do not make the backing array visible.
+ *
+ * <p>Note that bugs in this often require a sizeable input to reproduce; the default
+ * implementation of engineUpdate(ByteBuffer, ByteBuffer) copies through 4KB bounce buffers, so we
+ * need to use something larger to see any problems - 8KB is what we use here.
+ *
+ * @see https://bugs.openjdk.java.net/browse/JDK-8181386
+ */
+ @Test
+ public void testByteBufferShiftedAlias() throws Exception {
+ byte[] ptVector = new byte[8192];
+
+ for (int i = 0; i < 3; i++) {
+ // outputOffset = offset relative to start of input.
+ for (int outputOffset = -1; outputOffset <= 1; outputOffset++) {
+
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(new byte[16], "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ GCMParameterSpec parameters = new GCMParameterSpec(128, new byte[12]);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameters);
+
+ ByteBuffer output, input, inputRO;
+
+ // We'll try three scenarios: Ordinary array backed buffers, array backed buffers where one
+ // is read-only, and direct byte buffers.
+ String mode;
+ // offsets relative to start of buffer
+ int inputOffsetInBuffer = 1;
+ int outputOffsetInBuffer = inputOffsetInBuffer + outputOffset;
+ int sliceLength = cipher.getOutputSize(ptVector.length);
+ int bufferSize = sliceLength + Math.max(inputOffsetInBuffer, outputOffsetInBuffer);
+ switch (i) {
+ case 0:
+ case 1:
+ {
+ byte[] buffer = new byte[bufferSize];
+ // It's important to slice() here as otherwise later when we flip() position will be
+ // reset to 0.
+ output = ByteBuffer.wrap(buffer, outputOffsetInBuffer, sliceLength).slice();
+ input = ByteBuffer.wrap(buffer, inputOffsetInBuffer, sliceLength).slice();
+
+ if (i == 1) {
+ mode = "array backed buffers with RO buffer";
+ inputRO = input.asReadOnlyBuffer();
+ } else {
+ mode = "array backed buffers";
+ inputRO = input.duplicate();
+ }
+
+ break;
+ }
+ case 2:
+ {
+ mode = "direct buffers";
+ ByteBuffer buf = ByteBuffer.allocateDirect(bufferSize);
+ output = buf.duplicate();
+ output.position(outputOffsetInBuffer);
+ output.limit(sliceLength + outputOffsetInBuffer);
+ output = output.slice();
+
+ input = buf.duplicate();
+ input.position(inputOffsetInBuffer);
+ input.limit(sliceLength + inputOffsetInBuffer);
+ input = input.slice();
+
+ inputRO = input.duplicate();
+ break;
+ }
+ default:
+ {
+ throw new AssertionError("Unknown test index " + i);
+ }
+ }
+
+ // Now that we have our overlapping 'input' and 'output' buffers, we can write our plaintext
+ // into the input buffer.
+ input.put(ptVector);
+ input.flip();
+ // Make sure the RO input buffer has the same limit in case the plaintext is shorter than
+ // sliceLength (which it generally will be for anything other than ECB or CTR mode)
+ inputRO.limit(input.limit());
+
+ try {
+ int ctSize = cipher.doFinal(inputRO, output);
+
+ // Now flip the buffers around and undo everything
+ byte[] tmp = new byte[ctSize];
+ output.flip();
+ output.get(tmp);
+
+ output.clear();
+ input.clear();
+ inputRO.clear();
+
+ input.put(tmp);
+ input.flip();
+ inputRO.limit(input.limit());
+
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, parameters);
+ cipher.doFinal(inputRO, output);
+
+ output.flip();
+ assertEquals(ByteBuffer.wrap(ptVector), output);
+ } catch (Throwable t) {
+ throw new AssertionError(
+ "Overlapping buffers test failed with buffer type: "
+ + mode
+ + " and output offset "
+ + outputOffset,
+ t);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testReadOnlyByteBuffer() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ ByteBuffer ptBuffer = ByteBuffer.wrap(test.pt).asReadOnlyBuffer();
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.pt.length);
+ ByteBuffer ctBuffer = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ptBuffer, ctBuffer);
+ assertEquals(test.ctHex, TestUtil.byteBufferToHex(ctBuffer));
+
+ // Decryption
+ ctBuffer.flip();
+ ctBuffer = ctBuffer.asReadOnlyBuffer();
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ outputSize = cipher.getOutputSize(test.ct.length);
+ ByteBuffer decrypted = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ctBuffer, decrypted);
+ assertEquals(test.ptHex, TestUtil.byteBufferToHex(decrypted));
+ }
+ }
+
+ /**
+ * If a ByteBuffer is backed by an array and not readonly, then it is possible to access the data
+ * through the .array() method. An implementation using this possibility must ensure that it
+ * considers the offset.
+ */
+ @Test
+ public void testByteBufferWithOffset() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ ByteBuffer ptBuffer = ByteBuffer.wrap(new byte[test.pt.length + 50]);
+ ptBuffer.position(5);
+ ptBuffer = ptBuffer.slice();
+ ptBuffer.put(test.pt);
+ ptBuffer.flip();
+
+ ByteBuffer ctBuffer = ByteBuffer.wrap(new byte[test.ct.length + 50]);
+ ctBuffer.position(8);
+ ctBuffer = ctBuffer.slice();
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ptBuffer, ctBuffer);
+ assertEquals(test.ctHex, TestUtil.byteBufferToHex(ctBuffer));
+ ctBuffer.flip();
+
+ // Decryption
+ ByteBuffer decBuffer = ByteBuffer.wrap(new byte[test.pt.length + 50]);
+ decBuffer.position(6);
+ decBuffer = decBuffer.slice();
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ cipher.doFinal(ctBuffer, decBuffer);
+ assertEquals(test.ptHex, TestUtil.byteBufferToHex(decBuffer));
+ }
+ }
+
+ @Test
+ public void testByteBufferTooShort() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ ByteBuffer ptBuffer = ByteBuffer.wrap(test.pt);
+ ByteBuffer ctBuffer = ByteBuffer.allocate(test.ct.length - 1);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ try {
+ cipher.doFinal(ptBuffer, ctBuffer);
+ fail("This should not work");
+ } catch (ShortBufferException ex) {
+ // expected
+ }
+
+ // Decryption
+ ctBuffer = ByteBuffer.wrap(test.ct);
+ ByteBuffer decrypted = ByteBuffer.allocate(test.pt.length - 1);
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(test.aad);
+ try {
+ cipher.doFinal(ctBuffer, decrypted);
+ fail("This should not work");
+ } catch (ShortBufferException ex) {
+ // expected
+ }
+ }
+ }
+
+ /**
+ * Test encryption when update and doFinal are done with empty ByteBuffers. Conscrypt ignored
+ * calls to doFinal() when the ByteBuffer was empty.
+ */
+ @Test
+ public void testEncryptWithEmptyByteBuffer() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ // Encryption
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ ByteBuffer empty = ByteBuffer.allocate(0);
+ ByteBuffer ptBuffer = ByteBuffer.wrap(test.pt);
+ cipher.init(Cipher.ENCRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.pt.length);
+ ByteBuffer ctBuffer = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(empty);
+ cipher.updateAAD(test.aad);
+ cipher.update(empty, ctBuffer);
+ cipher.update(ptBuffer, ctBuffer);
+ cipher.doFinal(empty, ctBuffer);
+ assertEquals(test.ctHex, TestUtil.byteBufferToHex(ctBuffer));
+ }
+ }
+
+ @Test
+ public void testDecryptWithEmptyBuffer() throws Exception {
+ for (GcmTestVector test : getTestVectors()) {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ ByteBuffer empty = ByteBuffer.allocate(0);
+ ByteBuffer ctBuffer = ByteBuffer.wrap(test.ct);
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ int outputSize = cipher.getOutputSize(test.ct.length);
+ ByteBuffer ptBuffer = ByteBuffer.allocate(outputSize);
+ cipher.updateAAD(empty);
+ cipher.updateAAD(test.aad);
+ cipher.update(empty, ptBuffer);
+ cipher.update(ctBuffer, ptBuffer);
+ cipher.doFinal(empty, ptBuffer);
+ assertEquals(test.ptHex, TestUtil.byteBufferToHex(ptBuffer));
+
+ // Simple test that a modified ciphertext fails.
+ ctBuffer.flip();
+ ptBuffer.clear();
+ cipher.init(Cipher.DECRYPT_MODE, (SecretKey) keyStore.getKey(test.alias, null), test.parameters);
+ cipher.updateAAD(empty);
+ cipher.updateAAD(test.aad);
+ cipher.updateAAD(new byte[1]);
+ cipher.update(empty, ptBuffer);
+ cipher.update(ctBuffer, ptBuffer);
+ try {
+ cipher.doFinal(empty, ptBuffer);
+ fail("Accepted modified ciphertext.");
+ } catch (GeneralSecurityException ex) {
+ // Expected
+ }
+ }
+ }
+
+ /**
+ * The default authentication tag size should be 128-bit by default for the following reasons:
+ * <br>
+ * (1) Security: Ferguson, N., Authentication Weaknesses in GCM, Natl. Inst. Stand. Technol. [Web
+ * page], http://www.csrc.nist.gov/groups/ST/toolkit/BCM/documents/comments/
+ * CWC-GCM/Ferguson2.pdf, May 20, 2005. This paper points out that a n-bit tag has lower strength
+ * than expected. <br>
+ * (2) Compatibility: Assume an implementer tests some code using one provider than switches to
+ * another provider. Such a switch should ideally not lower the security. <br>
+ * Conscrypt used to have only 12-byte authentication tag (b/26186727).
+ */
+ @Test
+ @Ignore // IvParameterSpec is not supported in AndroidKeyStore AES/GCM cipher
+ public void testDefaultTagSizeIvParameterSpec() throws Exception {
+ byte[] counter = new byte[12];
+ byte[] input = new byte[16];
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(new byte[16], "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(counter));
+ } catch (InvalidAlgorithmParameterException ex) {
+ // OpenJDK8 does not support IvParameterSpec for GCM.
+ throw ex;
+ }
+ byte[] output = cipher.doFinal(input);
+ assertEquals(input.length + 16, output.length);
+ }
+
+ /**
+ * The default authentication tag size should be 128-bit by default for the following reasons:
+ * <br>
+ * (1) Security: Ferguson, N., Authentication Weaknesses in GCM, Natl. Inst. Stand. Technol. [Web
+ * page], http://www.csrc.nist.gov/groups/ST/toolkit/BCM/documents/comments/
+ * CWC-GCM/Ferguson2.pdf, May 20, 2005. This paper points out that a n-bit tag has lower strength
+ * than expected. <br>
+ * (2) Compatibility: Assume an implementer tests some code using one provider than switches to
+ * another provider. Such a switch should ideally not lower the security. <br>
+ * BouncyCastle used to have only 12-byte authentication tag (b/26186727).
+ */
+ @Test
+ @Ignore // GCM AlgorithmParameterGenerator is not available.
+ public void testDefaultTagSizeAlgorithmParameterGenerator() throws Exception {
+ byte[] input = new byte[10];
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(new byte[16], "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ try {
+ AlgorithmParameterGenerator.getInstance("GCM");
+ } catch (NoSuchAlgorithmException ex) {
+ // Conscrypt does not support AlgorithmParameterGenerator for GCM.
+ throw ex;
+ }
+ AlgorithmParameters param = AlgorithmParameterGenerator.getInstance("GCM").generateParameters();
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, param);
+ byte[] output = cipher.doFinal(input);
+ assertEquals(input.length + 16, output.length);
+ }
+
+ /**
+ * Test AES-GCM wrapped around counter bug which leaks plaintext and authentication key. Let's
+ * consider 12-byte IV, counter = IV || 0^31 || 1. For each encryption block, the last 4 bytes of
+ * the counter is increased by 1. After 2^32 blocks, the counter will be wrapped around causing
+ * counter collision and hence, leaking plaintext and authentication key as explained below. The
+ * library must make a check to make sure that the plaintext's length never exceeds 2^32 - 2
+ * blocks. Note that this is different from usual IV collisions because it happens even if users
+ * use different IVs. <br>
+ * We have: <br>
+ * J0 = IV || 0^31 || 1 <br>
+ * Plaintext: P[0], P[1], P[2], .... <br>
+ * Ciphertext: <br>
+ * C[0] = Enc(K, (J0 + 1) % 2^32) XOR P[0] <br>
+ * C[1] = Enc(K, (J0 + 2) % 2^32) XOR P[1] <br>
+ * C[2] = Enc(K, (J0 + 3) % 2^32) XOR P[2] <br>
+ * ... <br>
+ * C[2^32 - 1] = Enc(K, J0) XOR P[2^32 - 1] <br>
+ * C[2^32] = Enc(K, (J0 + 1)% 2^32) XOR P[2^32] <br>
+ * It means that after 2^32 blocks, the counter is wrapped around causing counter collisions. In
+ * counter mode, once the counter is collided then it's reasonable to assume that the plaintext is
+ * leaked. As the ciphertext is already known to attacker, Enc(K, J0) is leaked. <br>
+ * Now, as the authentication tag T is computed as GHASH(H, {}, C) XOR E(K, J0), the attacker can
+ * learn GHASH(H, {}, C}. It essentially means that the attacker finds a polynomial where H is the
+ * root (see Joux attack http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/Joux_comments.pdf).
+ * Solving polynomial equation in GF(2^128) is enough to extract the authentication key.
+ *
+ * <p>BouncyCastle used to have this bug (CVE-2015-6644).
+ *
+ * <p>OpenJDK8 used to have this bug (http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/0c3ed12cdaf5)
+ *
+ * <p>The test is slow as we have to encrypt 2^32 blocks.
+ */
+ @Test
+ @Ignore // This test takes very long time and CTS is timed out hence ignored for now.
+ public void testWrappedAroundCounter() throws Exception {
+ try {
+ byte[] iv = new byte[12];
+ byte[] input = new byte[16];
+ byte[] key = new byte[16];
+ (new SecureRandom()).nextBytes(key);
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ cipher.init(
+ Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(16 * 8, iv));
+ byte[] output = cipher.update(input);
+ for (long i = 0; i < 4294967296L + 2; i++) {
+ byte[] output1 = cipher.update(input);
+ assertFalse("GCM Wrapped Around Counter" + i, Arrays.equals(output, output1));
+ }
+ fail("Expected Exception");
+ } catch (Exception expected) {
+ }
+ }
+
+ /**
+ * AES-GCM allows IVs of bit length 1 .. 2^64-1. See NIST SP 800 38d, Section 5.2.1.1
+ * http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
+ *
+ * <p>Disallowing IVs of length 0 is necessary for the following reason: if an empty IV is used
+ * then the tag is an evaluation of a polynomial with the hash subkey as the value. Since the
+ * polynomial can be derived from the ciphertext it is known to an attacker. Therefore, any
+ * message encrypted with an empty IV leaks the hash subkey. In particular, encrypting an empty
+ * plaintext with an empty IV results in a ciphertext having a tag that is equal to the hash
+ * subkey used in AES-GCM. I.e. both are the same as encrypting an all zero block.
+ *
+ * <p>OpenJDK fails this test.
+ */
+ @Test
+ public void testEncryptEmptyPlaintextWithEmptyIv() throws Exception {
+ byte[] emptyIv = new byte[0];
+ byte[] input = new byte[0];
+ byte[] key = TestUtil.hexToBytes("56aae7bd5cbefc71d31c4338e6ddd6c5");
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ Cipher block = Cipher.getInstance("AES/ECB/NoPadding", EXPECTED_PROVIDER_NAME);
+ block.init(Cipher.ENCRYPT_MODE, secretKey);
+ byte[] hashkey = block.doFinal(new byte[16]);
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(16 * 8, emptyIv));
+ byte[] ct = cipher.doFinal(input);
+ // If the encryption above is not rejected then the hash key and the ciphertext are the same.
+ // Both are d1bdd948ddc5a7f7a9250cf78229b84d.
+ fail("Encrypting with an empty IV leaks the hash subkey.");
+ } catch (GeneralSecurityException expected) {
+ // expected behavior
+ }
+ }
+
+ @Test
+ public void testDecryptWithEmptyIv() throws Exception {
+ byte[] emptyIv = new byte[0];
+ byte[] key = TestUtil.hexToBytes("56aae7bd5cbefc71d31c4338e6ddd6c5");
+ SecretKey secretKey = null;
+ try {
+ String alias = "TestKey" + 1;
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+ secretKey = setKeystoreEntry(alias, keySpec);
+ } catch (Exception e) {
+ fail("Failed to set secret key entry in KeyStore.");
+ }
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", EXPECTED_PROVIDER_NAME);
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(16 * 8, emptyIv));
+ String ciphertext = "2b65876c00d77facf8f3d0e5be792b129bab10b25bcb739b92d6e2eab241245ff449";
+ String tag = "c2b2d7086e7fa84ca795a881b540";
+ byte[] pt1 = cipher.update(TestUtil.hexToBytes(ciphertext));
+ byte[] pt2 = cipher.doFinal(TestUtil.hexToBytes(tag));
+ // We shouldn't get here. If a provider releases unverified plaintext additionally to
+ // accepting empty IVs then chosen ciphertext attacks might be possible.
+ fail("AES-GCM must not accept an IV of size 0.");
+ } catch (GeneralSecurityException expected) {
+ //Expected
+ }
+ }
+}