Skip to content

Commit 8b92def

Browse files
committed
Grain-128AEAD: report an output buffer too short for a processBytes() or processByte() call as an OutputLengthException, as the general AEAD path does, rather than letting the write run off the end.
1 parent c76fa52 commit 8b92def

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎core/src/main/java/org/bouncycastle/crypto/engines/AEADBaseEngine.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ public int processByte(byte input, byte[] output, int outOff)
711711
boolean forEncryption = checkData(false);
712712
if (forEncryption)
713713
{
714+
ensureSufficientOutputBuffer(output, outOff, 1);
714715
this.len = 1;
715716
singleByte[0] = input;
716717
processBufferEncrypt(singleByte, 0, output, outOff);
@@ -720,6 +721,7 @@ public int processByte(byte input, byte[] output, int outOff)
720721
{
721722
if (m_bufPos == MAC_SIZE)
722723
{
724+
ensureSufficientOutputBuffer(output, outOff, 1);
723725
this.len = 1;
724726
processBufferDecrypt(m_buf, 0, output, outOff);
725727
System.arraycopy(m_buf, 1, m_buf, 0, m_bufPos - 1);
@@ -746,6 +748,10 @@ public int processBytes(byte[] input, int inOff, int len, byte[] output, int out
746748
boolean forEncryption = checkData(false);
747749
if (forEncryption)
748750
{
751+
if (len > 0)
752+
{
753+
ensureSufficientOutputBuffer(output, outOff, len);
754+
}
749755
this.len = len;
750756
processBufferEncrypt(input, inOff, output, outOff);
751757
return len;
@@ -754,6 +760,11 @@ public int processBytes(byte[] input, int inOff, int len, byte[] output, int out
754760
{
755761
// keep last mac size bytes
756762
int available = Math.max(m_bufPos + len - MAC_SIZE, 0);
763+
// checked before anything is written or buffered, and only when there is output
764+
if (available > 0)
765+
{
766+
ensureSufficientOutputBuffer(output, outOff, available);
767+
}
757768
int rlt = 0;
758769
if (m_bufPos > 0)
759770
{

‎core/src/test/java/org/bouncycastle/crypto/test/Grain128AEADTest.java‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.security.SecureRandom;
44

55
import org.bouncycastle.crypto.InvalidCipherTextException;
6+
import org.bouncycastle.crypto.OutputLengthException;
67
import org.bouncycastle.crypto.engines.Grain128AEADEngine;
78
import org.bouncycastle.crypto.modes.AEADCipher;
89
import org.bouncycastle.crypto.params.KeyParameter;
@@ -233,6 +234,56 @@ private void testExceptions()
233234
{
234235
isEquals("Grain-128 AEAD key must be 16 bytes long", e.getMessage());
235236
}
237+
238+
ParametersWithIV params = new ParametersWithIV(new KeyParameter(new byte[16]), new byte[12]);
239+
byte[] pt = new byte[64];
240+
241+
Grain128AEADEngine grain = new Grain128AEADEngine();
242+
243+
grain.init(true, params);
244+
245+
byte[] ct = new byte[grain.getOutputSize(pt.length)];
246+
int ctLen = grain.processBytes(pt, 0, pt.length, ct, 0);
247+
ctLen += grain.doFinal(ct, ctLen);
248+
249+
// an output buffer too short for what the call would write is reported the way the other
250+
// AEAD engines report it, rather than as an ArrayIndexOutOfBoundsException
251+
try
252+
{
253+
grain.init(true, params);
254+
grain.processBytes(pt, 0, pt.length, new byte[pt.length - 1], 0);
255+
fail("no exception");
256+
}
257+
catch (OutputLengthException e)
258+
{
259+
isEquals("output buffer too short", e.getMessage());
260+
}
261+
262+
try
263+
{
264+
grain.init(false, params);
265+
grain.processBytes(ct, 0, ctLen, new byte[pt.length - 1], 0);
266+
fail("no exception");
267+
}
268+
catch (OutputLengthException e)
269+
{
270+
isEquals("output buffer too short", e.getMessage());
271+
}
272+
273+
try
274+
{
275+
grain.init(true, params);
276+
grain.processByte((byte)1, new byte[0], 0);
277+
fail("no exception");
278+
}
279+
catch (OutputLengthException e)
280+
{
281+
isEquals("output buffer too short", e.getMessage());
282+
}
283+
284+
// nothing is released while the tag is still buffered, so the output is not touched
285+
grain.init(false, params);
286+
isEquals("buffered decryption releases nothing", 0, grain.processBytes(ct, 0, 8, new byte[0], 0));
236287
}
237288

238289
static void checkAEADCipherOutputSize(SimpleTest parent, int keySize, int ivSize, int tagSize, AEADCipher cipher)

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Date: 2026, TBD
2828
- An ERSDataGroup recomputed its hash on every request rather than taking it from the cache ERSCachingData exists to provide: the group overrode getHash(), which left the calculateHash() the cache calls unreachable - and wrong, as it copied the member hashes with a loop bounded by the size of the empty list it was copying into, so it would have digested nothing had anything reached it. The computation is back in calculateHash() and the override is gone, so a group's hash is computed once per digest algorithm and previous-chain hash, as every other ERSData's is. The value itself is unchanged.
2929
- ERSArchiveTimeStampGenerator rebuilt its reduced hash tree from the data objects on every call, so the usual generateTimeStampRequest() followed by generateArchiveTimeStamp() or generateArchiveTimeStamps() built it twice. The leaves are now built once and dropped when data or a previous chain is added. A Set of the data groups it had been given, which nothing ever read, has gone with it.
3030
- Grain-128AEAD returned corrupted plaintext from a decryption driven in chunks. The stream cipher data operator splits a processBytes() call that spans the buffered authentication tag into two output segments, the bytes released from the tag buffer and then the bytes taken straight from the caller's input, and wrote the second segment at the caller's output offset instead of after the first, so the second segment overwrote the head of the first and the tail of the reported output was never written at all. The call still returned the full byte count, and because the engine's state update depends on the input and the keystream rather than on where the output lands, the tag still verified: the wrong plaintext came back with no error raised. Any chunk after the first that carried more than the 8 byte tag length was affected. Grain-128AEAD is the only engine that uses this operator, and one shot decryption, the encryption path and every other AEAD engine were unaffected. The second segment is now written at the advanced offset, matching the equivalent step of the general decryption path (github PR #2447).
31+
- The AEAD stream cipher data operator, which Grain-128AEAD alone uses, wrote its output without first checking that the caller's buffer was long enough, so a short output buffer surfaced as an ArrayIndexOutOfBoundsException from inside the engine rather than as the OutputLengthException the general path reports for every other AEAD engine. Both directions of processBytes(), and processByte(), now check before anything is written or buffered, and only when the call releases output, as the general path does.
3132

3233
### 2.1.3 Additional Features and Functionality
3334

0 commit comments

Comments
 (0)