Skip to content

Strict output buffer requirements in AES/CBC decryption cause compatibility issues with SunJCE-based code #455

@alevymyers

Description

@alevymyers

Summary:
ACCP enforces strict buffer requirements when decrypting data using AES/CBC with padding (e.g., PKCS5Padding, PKCS7Padding). Specifically, it requires that the output buffer passed to Cipher.update() be large enough to hold the full decrypted plaintext output, including the final block that contains padding, as mentioned here in the code.

While this behavior is fully compliant with the JCA specification, it differs from the behavior of the default Java provider (SunJCE), which will buffer the final block internally and defer writing it until Cipher.doFinal() is called. SunJCE will subtract the padding when performing buffer size checks. This difference in behavior leads to compatibility issues with existing applications, including https://github.com/trinodb/trino.

Trino contains logic in the CompressingDecryptingPageDeserializer class that calls Cipher.update() during page deserialization with an output buffer sized based on assumptions consistent with SunJCE.

More specifically, it passes a buffer with a size equal to the size of the data to be decrypted, meaning it will get a java.crypto.ShortBufferException when padding is required.

Tests such as TestPagesSerde.testDeserializationWithRollover will fail due to this assumption.

This difference is not documented in the DIFFERENCES.md and it violates the statement made in the README.md

This means that it can be used as a drop in replacement for many different Java applications

The described behavior difference stems from how ACCP integrates with aws-lc native code and should be remedied. ACCP must always provide drop-in compatibility with JDK.

Reproducing the error in Trino:

  1. Clone Trino and checkout
  2. Build with ./mvnw clean install -DskipTests
  3. Run the failing test ./mvnw test -Dtest=io.trino.execution.buffer.TestPagesSerde -pl core/trino-main

Or, the following code sample highlights the difference where ACCP will throw a ShortBufferException while SunJCE will not:

byte[] keyBytes = new byte[16];
byte[] ivBytes = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(keyBytes);
random.nextBytes(ivBytes);

SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec iv = new IvParameterSpec(ivBytes);

byte[] plaintext = new byte[32];
random.nextBytes(plaintext);

Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptCipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] ciphertext = encryptCipher.doFinal(plaintext);

Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
decryptCipher.init(Cipher.DECRYPT_MODE, key, iv);

int outputSize = decryptCipher.getOutputSize(ciphertext.length - 10); // buffer.size < size + padding block

byte[] outputBuffer = new byte[outputSize];

int updateLen = decryptCipher.update(ciphertext, 0, ciphertext.length, outputBuffer, 0); // ACCP throws exception here
int finalLen = decryptCipher.doFinal(outputBuffer, updateLen);

int totalLen = updateLen + finalLen;

byte[] decrypted = Arrays.copyOf(outputBuffer, totalLen);
System.out.println("Decryption successful: " + Arrays.equals(plaintext, decrypted));

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions