2

The title pretty much sums up the question. The quadratic runtime of the cipher compared to the input, can be see using this sample code:

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.BiFunction;
import java.util.function.Function;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class AES_Test {

  public static void main(String[] args) throws Exception {
    Random r = new Random();
    byte[] key = new byte[32];
    byte[] spec = new byte[12];
    byte[] iv = new byte[16];
    r.nextBytes(key);
    r.nextBytes(spec);
    r.nextBytes(iv);

    List<BiFunction<Integer, SecretKeySpec, Cipher>> cipherCreators = List.of(
      (mode, serverKey) -> {
        GCMParameterSpec eGcmParameterSpec = new GCMParameterSpec(16 * 8, spec);
        try
        {
          Cipher eCipher = Cipher.getInstance("AES/GCM/NoPadding");
          eCipher.init(mode, serverKey, eGcmParameterSpec);
          return eCipher;
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
      },
      (mode, serverKey) -> {
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher eCipher;
        try {
          eCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
          eCipher.init(mode, serverKey, ivSpec);
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
        return eCipher;
      },
      (mode, serverKey) -> {
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher eCipher;
        try {
          eCipher = Cipher.getInstance("AES/CTR/NoPadding");
          eCipher.init(mode, serverKey, ivSpec);
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
        return eCipher;
      },
      (mode, serverKey) -> {
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher eCipher;
        try {
          eCipher = Cipher.getInstance("AES/CTS/NoPadding");
          eCipher.init(mode, serverKey, ivSpec);
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
        return eCipher;
      }
    );

    SecretKeySpec serverKey = new SecretKeySpec(key, "AES");
    GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, spec);

    for (int j = 0; j < 3; j++) {
      System.out.println("*** Run " + (j + 1) + " ***");
      for (BiFunction<Integer, SecretKeySpec, Cipher> cipherCreator : cipherCreators) {
        for (int i = 1; i <= 32; i *= 2) {
          byte[] randomBytes = new byte[i * 1024 * 1024];
          r.nextBytes(randomBytes);
            long start = System.currentTimeMillis();
          // Encrypt
          ByteArrayOutputStream bout = new ByteArrayOutputStream(randomBytes.length);
          {
            Cipher encryptCipher = cipherCreator.apply(Cipher.ENCRYPT_MODE, serverKey);
            ByteArrayInputStream fin = new ByteArrayInputStream(randomBytes);
            OutputStream cout = new CipherOutputStream(bout, encryptCipher);

            fin.transferTo(cout);
            cout.close();
          }
          byte[] encBytes = bout.toByteArray();
          long encrypted = System.currentTimeMillis();
          // Decrypt
          {
            InputStream fin = new ByteArrayInputStream(encBytes);
            Cipher decryptCipher = cipherCreator.apply(Cipher.DECRYPT_MODE, serverKey);
            InputStream cin = new CipherInputStream(fin, decryptCipher);
            bout = new ByteArrayOutputStream(randomBytes.length);

            cin.transferTo(bout);
          }
          long decrypted = System.currentTimeMillis();

          System.out.println(cipherCreator.apply(Cipher.ENCRYPT_MODE, serverKey).toString() + "  Size=" + i + "M  Encrypted=" + (encrypted - start) + "ms  Decrypted1=" + (decrypted - encrypted) + "ms result1=" + Arrays.equals(randomBytes, bout.toByteArray()));
        }
      }
    }
  }
}

On my machine, this gives:

*** Run 3 ***
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: SunJCE  Size=1M  Encrypted=13ms  Decrypted1=91ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: SunJCE  Size=2M  Encrypted=25ms  Decrypted1=236ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: SunJCE  Size=4M  Encrypted=56ms  Decrypted1=854ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: SunJCE  Size=8M  Encrypted=104ms  Decrypted1=3552ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: SunJCE  Size=16M  Encrypted=202ms  Decrypted1=13896ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: SunJCE  Size=32M  Encrypted=394ms  Decrypted1=53576ms result1=true

The O(n²) runtime of the decrypt is quite clear to see!

Looking at the code, it is buffering all input before it would push it out, but that should be O(lg(n)) as the buffer doubles in size each time to copy the input bytes.

It is something specific to the JDK implementation, since Bouncycastle does not exhibit this behaviour:

*** Run 3 ***
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: BC  Size=1M  Encrypted=15ms  Decrypted1=16ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: BC  Size=2M  Encrypted=28ms  Decrypted1=30ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: BC  Size=4M  Encrypted=51ms  Decrypted1=59ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: BC  Size=8M  Encrypted=111ms  Decrypted1=124ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: BC  Size=16M  Encrypted=196ms  Decrypted1=222ms result1=true
Cipher.AES/GCM/NoPadding, mode: encryption, algorithm from: BC  Size=32M  Encrypted=362ms  Decrypted1=443ms result1=true
Paul Wagland
  • 27,756
  • 10
  • 52
  • 74
  • 1
    Just a note, the `Cipher.toString()` method only started producing interesting output as of JDK 12. If you use JDK 11 or older you'll just get the useless output from `Object.toString()`. – President James K. Polk Nov 25 '22 at 18:49
  • I don't have time right now to run experiments but I would guess that the Oracle provider "correctly" embargoes the plaintext until the tag is verified whereas the BC provider does not. The extra memory requirements somehow causes some kind of performance issue, perhaps GC related. There is some java command-line to increase the initial heap allocation, try setting it to 1GB and see if the results are the same. – President James K. Polk Nov 25 '22 at 20:24
  • 1
    When I drastically simplify the code, getting rid of all streams, defining three 32MB arrays `randomBytes`, `encBytes`, and `decryptedBytes` outside of all the loops, and just calling [`cipher.doFinal()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/javax/crypto/Cipher.html#doFinal(byte%5B%5D,int,int,byte%5B%5D)) with offset and length arguments, the n^2 behavior disappears and everything is much faster. I won't post it an answer because it does not answer the question, but it is a data point. – President James K. Polk Nov 26 '22 at 02:03
  • 1
    @PresidentJamesK.Polk – AFAICT, BouncyCastle also defers the return of the first byte until the entire stream is verified. – Paul Wagland Nov 28 '22 at 11:36
  • 1
    I have created a bug with Oracle, with internal review ID : 9074388. I'll update this issue once this is accepted. – Paul Wagland Nov 28 '22 at 11:50
  • 1
    Issue has been made public: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8298249 – Paul Wagland Dec 07 '22 at 16:22
  • Thanks Paul, please continue to post updates as they become available. – President James K. Polk Dec 07 '22 at 22:30
  • 1
    According to latest updates on the issue, looks like this will get fixed in Java 21. – Paul Wagland Jan 03 '23 at 23:08

0 Answers0