F-5033: grow update() buffer by doubling in WolfCryptCipher

pull/250/head
Chris Conlon 2026-07-30 15:07:17 -06:00
parent 90c50b850b
commit c19e3f0fa1
2 changed files with 295 additions and 28 deletions

View File

@ -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(

View File

@ -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.