Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ public int processBytes(byte[] input, int inOff, int len, byte[] output, int out
if (available > 0)
{
this.len = available;
processBufferDecrypt(input, inOff, output, outOff);
processBufferDecrypt(input, inOff, output, outOff + rlt);
rlt += available;
len -= available;
inOff += available;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ public AEADCipher createInstance()
}
});
CipherTest.checkAEADCipherMultipleBlocks(this, 1024, 7, 100, 128, 12, new Grain128AEADEngine());

// PARTLEN has to exceed the 8 byte tag length for a single processBytes() call to release
// both buffered tag bytes and bytes taken straight from the input.
CipherTest.checkAEADCipherMultipleBlocks(this, 1024, 19, 100, 128, 12, new Grain128AEADEngine());

CipherTest.checkAEADParemeter(this, 16, 12, 8, 20, new Grain128AEADEngine());

testSplitUpdate();
testExceptions();
testLongAEAD();
testStreamedDecryption();
}


Expand Down Expand Up @@ -141,6 +144,57 @@ private void testLongAEAD()
}
}

private void testStreamedDecryption()
throws InvalidCipherTextException
{
byte[] key = Hex.decode("000102030405060708090A0B0C0D0E0F");
byte[] nonce = Hex.decode("000102030405060708090A0B");
byte[] ad = Hex.decode("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E");
byte[] pt = new byte[64];
for (int i = 0; i != pt.length; i++)
{
pt[i] = (byte)i;
}

ParametersWithIV params = new ParametersWithIV(new KeyParameter(key), nonce);
Grain128AEADEngine grain = new Grain128AEADEngine();

grain.init(true, params);
grain.processAADBytes(ad, 0, ad.length);
byte[] ct = new byte[grain.getOutputSize(pt.length)];
int ctLen = grain.processBytes(pt, 0, pt.length, ct, 0);
ctLen += grain.doFinal(ct, ctLen);
isEquals("cipher text length", ct.length, ctLen);

grain.init(false, params);
grain.processAADBytes(ad, 0, ad.length);
byte[] oneShot = new byte[grain.getOutputSize(ctLen)];
int oneShotLen = grain.processBytes(ct, 0, ctLen, oneShot, 0);
oneShotLen += grain.doFinal(oneShot, oneShotLen);
isEquals("one shot plain text length", pt.length, oneShotLen);
isTrue("one shot decryption", Arrays.areEqual(pt, oneShot));

// A processBytes() call that releases buffered tag bytes and bytes taken straight from the
// input writes its output in two segments. Split the cipher text at every point so that the
// second chunk exceeds the 8 byte tag length and both segments are written by one call.
// doFinal() throws when the tag does not verify, so reaching the comparisons means the tag
// was accepted and only the plain text placement is under test.
for (int split = 1; split != ctLen; split++)
{
grain.init(false, params);
grain.processAADBytes(ad, 0, ad.length);

byte[] streamed = new byte[grain.getOutputSize(ctLen)];
int len = grain.processBytes(ct, 0, split, streamed, 0);
len += grain.processBytes(ct, split, ctLen - split, streamed, len);
len += grain.doFinal(streamed, len);

isEquals("streamed plain text length, split " + split, pt.length, len);
isTrue("streamed decryption does not match one shot, split " + split, Arrays.areEqual(oneShot, streamed));
isTrue("streamed decryption does not match plain text, split " + split, Arrays.areEqual(pt, streamed));
}
}

private void testExceptions()
throws InvalidCipherTextException
{
Expand Down
2 changes: 2 additions & 0 deletions docs/releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Date: 2026, TBD

- The name-constraint host canonicalisation removed a single RFC 1034 root-label dot, the only empty label a name may legally carry, but nothing refused the ones that are not legal: a dNSName, rfc822Name host or uniformResourceIdentifier host such as "example.com.." kept a phantom empty label after the strip and so matched no constraint at all, escaping an excluded subtree naming the host it appears to carry. A tested name whose host carries an empty label - a second trailing dot, a doubled dot or a leading dot - is now refused outright wherever a constraint of that type is in force, rather than canonicalised into a name it is not: removing the extra dots would decide on the caller's behalf that "example.com.." names example.com, which is not how a consumer resolving or comparing the name reads it, and refusing fails closed in both directions where canonicalising would newly admit such a name under a permitted subtree. The single trailing dot is canonicalised as before, a bare "." remains the root label rather than an empty one, and the guard is scoped to the host, so the doubled dot a quoted local part may legally carry is unaffected. Constraints are untouched - one may still begin with a dot, which is how this implementation spells "subdomains only" (github PR #2436).

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

### 2.1.3 Additional Features and Functionality

### 2.1.4 Additional Notes
Expand Down