From 730842ac8c39be6080a867bbfd163fae660c5248 Mon Sep 17 00:00:00 2001 From: bdonlan Date: Tue, 13 Jun 2017 16:43:50 -0700 Subject: [PATCH] Add tests for partially-overlapping input/output buffers (#29) update/doFinal are required to be "copy-safe". From the javadoc: Note: this method should be copy-safe, which means the input and output buffers can reference the same block of memory and no unprocessed input data is overwritten when the result is copied into the output buffer. However many implementations of JCE methods do not implement this property in all cases. Notably, BouncyCastle fails this test on arrays, and the built-in OpenJDK providers fail when the input bytebuffer is either a read-only array-backed byte buffer, or if the buffers are direct buffers. For SunJCE, this has been acknowledged publicly by Oracle at https://bugs.openjdk.java.net/browse/JDK-8181386 . These new tests verify correct overlapping buffer handling for AES/GCM specifically; as future work, it would be good to generalize these tests for other ciphers and modes. --- .../wycheproof/testcases/AesGcmTest.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) 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