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.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user