Merge pull request #250 from cconlon/fenrirJuly30
Fenrir fixes for WolfCryptCipher update()pull/252/head
commit
f520b03a7f
|
|
@ -174,8 +174,93 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
/* Has this Cipher been inintialized? */
|
||||
private boolean cipherInitialized = false;
|
||||
|
||||
/* buffered data from update calls */
|
||||
/* Buffered data from update calls, only the first bufferedLen bytes
|
||||
* are valid. Capacity grows to max(2 * capacity, needed). */
|
||||
private byte[] buffered = new byte[0];
|
||||
private int bufferedLen = 0;
|
||||
|
||||
/* Max buffered size. Below Integer.MAX_VALUE, which is not allocatable
|
||||
* in full because VMs reserve array header words. */
|
||||
private static final int MAX_BUFFERED_SIZE = Integer.MAX_VALUE - 8;
|
||||
|
||||
/* Capacity above this is released on reset instead of kept, so one
|
||||
* large message does not pin memory for the life of the Cipher. */
|
||||
private static final int MAX_RETAINED_SIZE = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Append len bytes of src to the buffered data.
|
||||
*
|
||||
* Any buffer replaced during growth is zeroized before being dropped.
|
||||
*
|
||||
* @param src array to append from
|
||||
* @param offset offset into src to start at
|
||||
* @param len number of bytes to append
|
||||
*
|
||||
* @throws IllegalArgumentException if the total buffered size would
|
||||
* exceed MAX_BUFFERED_SIZE
|
||||
*/
|
||||
private void bufferedAppend(byte[] src, int offset, int len) {
|
||||
|
||||
int newCap, needed;
|
||||
|
||||
if (len > (MAX_BUFFERED_SIZE - this.bufferedLen)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Buffered input would exceed maximum size of " +
|
||||
MAX_BUFFERED_SIZE + " bytes");
|
||||
}
|
||||
needed = this.bufferedLen + len;
|
||||
|
||||
if (needed > this.buffered.length) {
|
||||
newCap = this.buffered.length * 2;
|
||||
if (newCap < 0 || newCap > MAX_BUFFERED_SIZE) {
|
||||
/* Doubling overflowed or passed the cap, pin to max */
|
||||
newCap = MAX_BUFFERED_SIZE;
|
||||
}
|
||||
if (newCap < needed) {
|
||||
/* Single append larger than double, cap at needed */
|
||||
newCap = needed;
|
||||
}
|
||||
|
||||
byte[] tmp = new byte[newCap];
|
||||
System.arraycopy(this.buffered, 0, tmp, 0, this.bufferedLen);
|
||||
Arrays.fill(this.buffered, 0, this.bufferedLen, (byte)0);
|
||||
this.buffered = tmp;
|
||||
}
|
||||
|
||||
System.arraycopy(src, offset, this.buffered, this.bufferedLen, len);
|
||||
this.bufferedLen = needed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop and zeroize all buffered data.
|
||||
*
|
||||
* Bytes at or past bufferedLen are always zero already, so only the
|
||||
* used prefix is cleared. Capacity is kept for reuse unless it is
|
||||
* above MAX_RETAINED_SIZE, which is released instead.
|
||||
*/
|
||||
private void bufferedReset() {
|
||||
|
||||
Arrays.fill(this.buffered, 0, this.bufferedLen, (byte)0);
|
||||
this.bufferedLen = 0;
|
||||
|
||||
if (this.buffered.length > MAX_RETAINED_SIZE) {
|
||||
this.buffered = new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the first count bytes, shifting the remainder to the front.
|
||||
*
|
||||
* @param count bytes to drop, must be between 0 and bufferedLen
|
||||
*/
|
||||
private void bufferedConsume(int count) {
|
||||
|
||||
int remaining = this.bufferedLen - count;
|
||||
|
||||
System.arraycopy(this.buffered, count, this.buffered, 0, remaining);
|
||||
Arrays.fill(this.buffered, remaining, this.bufferedLen, (byte)0);
|
||||
this.bufferedLen = remaining;
|
||||
}
|
||||
|
||||
private WolfCryptCipher(CipherType type, CipherMode mode,
|
||||
PaddingType pad) {
|
||||
|
|
@ -609,8 +694,8 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
|
||||
/* Add buffered data size to input length, calculate total blocks */
|
||||
if (isBlockCipher()) {
|
||||
if (buffered != null && buffered.length > 0) {
|
||||
totalSz = inputLen + buffered.length;
|
||||
if (bufferedLen > 0) {
|
||||
totalSz = inputLen + bufferedLen;
|
||||
} else {
|
||||
totalSz = inputLen;
|
||||
}
|
||||
|
|
@ -654,8 +739,8 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
}
|
||||
else if (paddingType == PaddingType.WC_PKCS5) {
|
||||
outSize = inputLen;
|
||||
if (buffered != null && buffered.length > 0) {
|
||||
outSize += buffered.length;
|
||||
if (bufferedLen > 0) {
|
||||
outSize += bufferedLen;
|
||||
}
|
||||
/* Only add padding size when encrypting. When decrypting,
|
||||
* the output size should not include padding bytes since
|
||||
|
|
@ -679,8 +764,8 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
}
|
||||
else if (paddingType == PaddingType.WC_PKCS5) {
|
||||
outSize = inputLen;
|
||||
if (buffered != null && buffered.length > 0) {
|
||||
outSize += buffered.length;
|
||||
if (bufferedLen > 0) {
|
||||
outSize += bufferedLen;
|
||||
}
|
||||
/* Only add padding size when encrypting. When decrypting,
|
||||
* the output size should not include padding bytes since
|
||||
|
|
@ -1203,7 +1288,7 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
throws InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
|
||||
/* Reset buffered data from any previous operation */
|
||||
buffered = new byte[0];
|
||||
bufferedReset();
|
||||
|
||||
InitializeNativeStructs();
|
||||
wolfCryptSetDirection(opmode);
|
||||
|
|
@ -1340,7 +1425,6 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
int bytesToProcess = 0;
|
||||
byte[] output = null;
|
||||
byte[] tmpIn = null;
|
||||
byte[] tmpBuf = null;
|
||||
|
||||
if (input == null || len < 0 || inputOffset < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
|
|
@ -1363,34 +1447,31 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
|
||||
this.operationStarted = true;
|
||||
|
||||
if ((buffered.length + len) == 0) {
|
||||
if ((bufferedLen + len) == 0) {
|
||||
/* no data to process */
|
||||
return null;
|
||||
}
|
||||
|
||||
if (len > 0) {
|
||||
/* add input bytes to buffered */
|
||||
tmpIn = new byte[buffered.length + len];
|
||||
System.arraycopy(buffered, 0, tmpIn, 0, buffered.length);
|
||||
System.arraycopy(input, inputOffset, tmpIn, buffered.length, len);
|
||||
buffered = tmpIn;
|
||||
bufferedAppend(input, inputOffset, len);
|
||||
}
|
||||
|
||||
/* Some algos/modes keep data buffered until the doFinal() call, like
|
||||
* RSA or AES-GCM/CCM without stream mode compiled natively. Just
|
||||
* return an empty byte array in those cases here. */
|
||||
if (isNoOpUpdate(len + buffered.length)) {
|
||||
if (isNoOpUpdate(bufferedLen)) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/* Calculate blocks and partial non-block size remaining */
|
||||
blocks = buffered.length / blockSize;
|
||||
blocks = bufferedLen / blockSize;
|
||||
bytesToProcess = blocks * blockSize;
|
||||
|
||||
/* CTR and OFB are stream ciphers, process all available data */
|
||||
if (cipherMode == CipherMode.WC_CTR ||
|
||||
cipherMode == CipherMode.WC_OFB) {
|
||||
bytesToProcess = buffered.length;
|
||||
bytesToProcess = bufferedLen;
|
||||
}
|
||||
|
||||
/* If PKCS#5/7 padding, and decrypting, hold on to last block for
|
||||
|
|
@ -1409,10 +1490,8 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
tmpIn = new byte[bytesToProcess];
|
||||
System.arraycopy(buffered, 0, tmpIn, 0, bytesToProcess);
|
||||
|
||||
/* buffer remaining non-block size input, or reset */
|
||||
tmpBuf = new byte[buffered.length - bytesToProcess];
|
||||
System.arraycopy(buffered, bytesToProcess, tmpBuf, 0, tmpBuf.length);
|
||||
buffered = tmpBuf;
|
||||
/* keep remaining non-block size input buffered */
|
||||
bufferedConsume(bytesToProcess);
|
||||
|
||||
/* process tmpIn[] */
|
||||
switch (this.cipherType) {
|
||||
|
|
@ -1499,7 +1578,7 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
byte tmpOut[] = null;
|
||||
|
||||
this.operationStarted = true;
|
||||
totalSz = buffered.length + len;
|
||||
totalSz = bufferedLen + len;
|
||||
|
||||
/* AES-CTS requires input length >= 16 bytes (RFC 3962/8009).
|
||||
* For exactly 16 bytes, CTS reduces to plain CBC, handled in JNI. */
|
||||
|
|
@ -1523,16 +1602,16 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
(totalSz % blockSize != 0)) {
|
||||
throw new IllegalBlockSizeException(
|
||||
"Input length (" + totalSz + ") not multiple of " +
|
||||
blockSize + " bytes. (" + buffered.length +" buffered)");
|
||||
blockSize + " bytes. (" + bufferedLen +" buffered)");
|
||||
}
|
||||
|
||||
/* do final encrypt over totalSz */
|
||||
tmpIn = new byte[totalSz];
|
||||
if (totalSz > 0) {
|
||||
System.arraycopy(buffered, 0, tmpIn, 0, buffered.length);
|
||||
System.arraycopy(buffered, 0, tmpIn, 0, bufferedLen);
|
||||
if (input != null && len > 0) {
|
||||
System.arraycopy(input, inputOffset, tmpIn,
|
||||
buffered.length, len);
|
||||
bufferedLen, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1779,7 +1858,7 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
|
||||
/* reset state, user doesn't need to call init again before use */
|
||||
try {
|
||||
buffered = new byte[0];
|
||||
bufferedReset();
|
||||
|
||||
wolfCryptSetDirection(this.storedOpMode);
|
||||
|
||||
|
|
@ -1943,7 +2022,7 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
}
|
||||
|
||||
log("final (offset: " + inputOffset + ", len: " + inputLen +
|
||||
", buffered: " + buffered.length + ")");
|
||||
", buffered: " + bufferedLen + ")");
|
||||
|
||||
return wolfCryptFinal(input, inputOffset, inputLen);
|
||||
}
|
||||
|
|
@ -1963,7 +2042,7 @@ public class WolfCryptCipher extends CipherSpi {
|
|||
|
||||
log("final (inputOffset: " + inputOffset + ", inputLen: " +
|
||||
inputLen + ", outputOffset: " + outputOffset + ", buffered: " +
|
||||
buffered.length + ")");
|
||||
bufferedLen + ")");
|
||||
|
||||
if (output == null || (outputOffset > output.length)) {
|
||||
throw new IllegalArgumentException(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import org.junit.runner.Description;
|
|||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Random;
|
||||
|
|
@ -2304,6 +2305,193 @@ public class WolfCryptCipherTest {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that many small update() calls match a single call. GCM buffers
|
||||
* everything until doFinal(), so this drives buffer growth only.
|
||||
*/
|
||||
@Test
|
||||
public void testAesGcmChunkedUpdateMatchesSingle() throws Exception {
|
||||
|
||||
if (!enabledJCEAlgos.contains("AES/GCM/NoPadding") ||
|
||||
!FeatureDetect.Aes256Enabled()) {
|
||||
/* skip if AES-256-GCM is not enabled */
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] keyBytes = new byte[32];
|
||||
byte[] iv = new byte[12];
|
||||
byte[] plaintext = new byte[4096];
|
||||
|
||||
for (int i = 0; i < keyBytes.length; i++) {
|
||||
keyBytes[i] = (byte)i;
|
||||
}
|
||||
for (int i = 0; i < iv.length; i++) {
|
||||
iv[i] = (byte)(0xB0 + i);
|
||||
}
|
||||
for (int i = 0; i < plaintext.length; i++) {
|
||||
plaintext[i] = (byte)(i & 0xFF);
|
||||
}
|
||||
|
||||
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
|
||||
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
|
||||
|
||||
Cipher single = Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
|
||||
single.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
byte[] expected = single.doFinal(plaintext);
|
||||
|
||||
/* Same plaintext, sent one byte per update() call */
|
||||
Cipher chunked = Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
|
||||
chunked.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
for (int i = 0; i < plaintext.length; i++) {
|
||||
chunked.update(plaintext, i, 1);
|
||||
}
|
||||
|
||||
assertArrayEquals("Chunked update should match single doFinal",
|
||||
expected, chunked.doFinal());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test reusing a Cipher after a message large enough that the internal
|
||||
* buffer capacity is released rather than kept. Drives the release and
|
||||
* regrow path, where a stale buffered length would corrupt the next
|
||||
* message.
|
||||
*/
|
||||
@Test
|
||||
public void testAesGcmLargeThenSmallReusesCipher() throws Exception {
|
||||
|
||||
if (!enabledJCEAlgos.contains("AES/GCM/NoPadding") ||
|
||||
!FeatureDetect.Aes256Enabled()) {
|
||||
/* skip if AES-256-GCM is not enabled */
|
||||
return;
|
||||
}
|
||||
|
||||
/* Larger than the buffer capacity kept across operations */
|
||||
byte[] large = new byte[256 * 1024];
|
||||
byte[] small = new byte[64];
|
||||
new Random(1234).nextBytes(large);
|
||||
new Random(5678).nextBytes(small);
|
||||
|
||||
SecretKeySpec key = new SecretKeySpec(new byte[32], "AES");
|
||||
byte[] iv = new byte[12];
|
||||
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
|
||||
|
||||
/* Large message fed in chunks, so capacity grows well past the
|
||||
* retention limit */
|
||||
c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
|
||||
for (int i = 0; i < large.length; i += 4096) {
|
||||
c.update(large, i, Math.min(4096, large.length - i));
|
||||
}
|
||||
byte[] largeCipher = c.doFinal();
|
||||
|
||||
/* Same instance, smaller message with a different IV */
|
||||
iv[0] = (byte)0x01;
|
||||
c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
|
||||
byte[] smallCipher = c.doFinal(small);
|
||||
|
||||
/* Both must match a fresh Cipher doing the same work */
|
||||
Cipher refLarge = Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
|
||||
byte[] iv0 = new byte[12];
|
||||
refLarge.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv0));
|
||||
assertArrayEquals("Large message should match fresh Cipher",
|
||||
refLarge.doFinal(large), largeCipher);
|
||||
|
||||
Cipher refSmall = Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
|
||||
refSmall.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
|
||||
assertArrayEquals("Reused Cipher should match fresh Cipher",
|
||||
refSmall.doFinal(small), smallCipher);
|
||||
|
||||
/* Round trip the reused instance output */
|
||||
Cipher dec = Cipher.getInstance("AES/GCM/NoPadding", jceProvider);
|
||||
dec.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
|
||||
assertArrayEquals("Reused Cipher output should decrypt",
|
||||
small, dec.doFinal(smallCipher));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test CBC round trips with non block aligned update() sizes. Unlike
|
||||
* GCM, CBC drains the buffer each call, so this drives the grow then
|
||||
* partial consume path that shifts leftover bytes to the front.
|
||||
*/
|
||||
@Test
|
||||
public void testAesCbcUnalignedChunkedUpdateRoundTrip()
|
||||
throws Exception {
|
||||
|
||||
if (!enabledJCEAlgos.contains("AES/CBC/NoPadding") ||
|
||||
!enabledJCEAlgos.contains("AES/CBC/PKCS5Padding")) {
|
||||
/* skip if AES-CBC is not enabled */
|
||||
return;
|
||||
}
|
||||
|
||||
/* Sizes that leave a partial block buffered across most calls */
|
||||
int[] chunkSizes = { 1, 3, 7, 13, 31, 127, 5, 999, 17 };
|
||||
int[] inputSizes = { 4096, 64 * 1024, 300 * 1024 };
|
||||
String[] transforms =
|
||||
{ "AES/CBC/NoPadding", "AES/CBC/PKCS5Padding" };
|
||||
|
||||
SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
|
||||
IvParameterSpec iv = new IvParameterSpec(new byte[16]);
|
||||
|
||||
for (String transform : transforms) {
|
||||
for (int size : inputSizes) {
|
||||
|
||||
byte[] plaintext = new byte[size];
|
||||
new Random(size).nextBytes(plaintext);
|
||||
|
||||
Cipher enc = Cipher.getInstance(transform, jceProvider);
|
||||
enc.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
byte[] expected = enc.doFinal(plaintext);
|
||||
|
||||
/* Encrypt again, feeding rotating unaligned chunk sizes */
|
||||
enc.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int offset = 0;
|
||||
int idx = 0;
|
||||
while (offset < plaintext.length) {
|
||||
int n = chunkSizes[idx % chunkSizes.length];
|
||||
idx++;
|
||||
if (n > (plaintext.length - offset)) {
|
||||
n = plaintext.length - offset;
|
||||
}
|
||||
byte[] part = enc.update(plaintext, offset, n);
|
||||
if (part != null) {
|
||||
out.write(part);
|
||||
}
|
||||
offset += n;
|
||||
}
|
||||
out.write(enc.doFinal());
|
||||
byte[] chunkedCipher = out.toByteArray();
|
||||
|
||||
assertArrayEquals(transform + " chunked encrypt at " + size +
|
||||
" should match single doFinal", expected, chunkedCipher);
|
||||
|
||||
/* Decrypt the same way and confirm the round trip */
|
||||
Cipher dec = Cipher.getInstance(transform, jceProvider);
|
||||
dec.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
ByteArrayOutputStream plain = new ByteArrayOutputStream();
|
||||
offset = 0;
|
||||
idx = 0;
|
||||
while (offset < chunkedCipher.length) {
|
||||
int n = chunkSizes[idx % chunkSizes.length];
|
||||
idx++;
|
||||
if (n > (chunkedCipher.length - offset)) {
|
||||
n = chunkedCipher.length - offset;
|
||||
}
|
||||
byte[] part = dec.update(chunkedCipher, offset, n);
|
||||
if (part != null) {
|
||||
plain.write(part);
|
||||
}
|
||||
offset += n;
|
||||
}
|
||||
plain.write(dec.doFinal());
|
||||
|
||||
assertArrayEquals(transform + " chunked decrypt at " + size +
|
||||
" should recover plaintext", plaintext,
|
||||
plain.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that AAD supplied over many updateAAD() calls produces the same
|
||||
* result as a single call. Guards the incremental AAD accumulator.
|
||||
|
|
|
|||
Loading…
Reference in New Issue