diff --git a/java/com/google/security/wycheproof/testcases/AesGcmTest.java b/java/com/google/security/wycheproof/testcases/AesGcmTest.java index 4e4a879..a9bca6d 100644 --- a/java/com/google/security/wycheproof/testcases/AesGcmTest.java +++ b/java/com/google/security/wycheproof/testcases/AesGcmTest.java @@ -16,6 +16,8 @@ package com.google.security.wycheproof; +import static org.junit.Assert.assertArrayEquals; + import com.google.security.wycheproof.WycheproofRunner.ExcludedTest; import com.google.security.wycheproof.WycheproofRunner.ProviderType; import com.google.security.wycheproof.WycheproofRunner.SlowTest; @@ -268,6 +270,172 @@ public class AesGcmTest extends TestCase { } } + /** Encryption and decryption with large arrays should be copy-safe. */ + 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++) { + SecretKeySpec key = new SecretKeySpec(new byte[16], "AES"); + GCMParameterSpec parameters = new GCMParameterSpec(128, new byte[16]); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, 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"); + cipher.init(Cipher.DECRYPT_MODE, key, 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. + * + * 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 + */ + 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++) { + + SecretKeySpec key = new SecretKeySpec(new byte[16], "AES"); + GCMParameterSpec parameters = new GCMParameterSpec(128, new byte[16]); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, 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, key, 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); + } + } + } + + } + public void testReadOnlyByteBuffer() throws Exception { for (GcmTestVector test : getTestVectors()) { // Encryption