Skip to content

Commit 12eac54

Browse files
committed
Count buffered bytes in the AEADBaseEngine in-place input/output overlap check
1 parent 6d7d611 commit 12eac54

4 files changed

Lines changed: 148 additions & 1 deletion

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1066,7 +1066,7 @@ protected int processEncDecBytes(byte[] input, int inOff, int len, byte[] output
10661066
resultLength = length + m_bufPos - (forEncryption ? 0 : MAC_SIZE);
10671067
ensureSufficientOutputBuffer(output, outOff, resultLength - resultLength % BlockSize);
10681068
resultLength = 0;
1069-
if (input == output && Arrays.segmentsOverlap(inOff, len, outOff, length))
1069+
if (input == output && Arrays.segmentsOverlap(inOff, len, outOff, length + m_bufPos))
10701070
{
10711071
input = new byte[len];
10721072
System.arraycopy(output, inOff, input, 0, len);

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,5 +942,78 @@ static void testOverlapping(SimpleTest test, int keySize, int ivSize, int macSiz
942942
test.isTrue("fail on testing overlapping of decryption for " + cipher.getAlgorithmName(),
943943
Arrays.areEqual(expected, 0, blockSize * 2, data, offset, offset + blockSize * 2));
944944

945+
testOverlappingSplit(test, keySize, ivSize, macSize, blockSize, cipher);
946+
}
947+
948+
/**
949+
* Encrypt and decrypt in two processBytes() calls with the input and the output in the same array, the
950+
* output starting at, behind or ahead of the input, and compare with a single call into a separate array.
951+
* The second call writes the bytes the first one buffered as well as its own, so its overlap check has to
952+
* allow for them.
953+
*/
954+
static void testOverlappingSplit(SimpleTest test, int keySize, int ivSize, int macSize, int blockSize, AEADCipher cipher)
955+
throws Exception
956+
{
957+
AEADParameters parameters = new AEADParameters(new KeyParameter(new byte[keySize]), macSize * 8, new byte[ivSize], null);
958+
int[] lags = new int[]{ -blockSize, 1 - blockSize, -1, 0, 1, blockSize };
959+
for (int dataLen = 2; dataLen <= blockSize * 3 + 2; dataLen++)
960+
{
961+
byte[] data = new byte[dataLen];
962+
for (int i = 0; i != dataLen; i++)
963+
{
964+
data[i] = (byte)i;
965+
}
966+
cipher.init(true, parameters);
967+
byte[] expected = new byte[cipher.getOutputSize(dataLen)];
968+
int len = cipher.processBytes(data, 0, dataLen, expected, 0);
969+
cipher.doFinal(expected, len);
970+
971+
for (int i = 0; i != lags.length; i++)
972+
{
973+
for (int split = 1; split < data.length; split++)
974+
{
975+
checkOverlappingSplit(test, true, parameters, data, expected, split, lags[i], blockSize + macSize, cipher);
976+
}
977+
for (int split = 1; split < expected.length; split++)
978+
{
979+
checkOverlappingSplit(test, false, parameters, expected, data, split, lags[i], blockSize + macSize, cipher);
980+
}
981+
}
982+
}
983+
}
984+
985+
private static void checkOverlappingSplit(SimpleTest test, boolean forEncryption, AEADParameters parameters, byte[] input,
986+
byte[] expected, int split, int lag, int margin, AEADCipher cipher)
987+
throws Exception
988+
{
989+
if (lag > 0)
990+
{
991+
// with the output ahead of the input, a first call returning more than split - lag bytes would
992+
// overwrite input the caller has not passed in yet, which is a caller error
993+
cipher.init(forEncryption, parameters);
994+
if (lag + cipher.processBytes(input, 0, split, new byte[input.length + margin], 0) > split)
995+
{
996+
return;
997+
}
998+
}
999+
String label = (forEncryption ? "encryption" : "decryption") + " for " + cipher.getAlgorithmName()
1000+
+ ", length " + input.length + " split " + split + " lag " + lag;
1001+
int inOff = margin;
1002+
int outOff = inOff + lag;
1003+
byte[] buf = new byte[inOff + input.length + 2 * margin];
1004+
System.arraycopy(input, 0, buf, inOff, input.length);
1005+
cipher.init(forEncryption, parameters);
1006+
int len = cipher.processBytes(buf, inOff, split, buf, outOff);
1007+
len += cipher.processBytes(buf, inOff + split, input.length - split, buf, outOff + len);
1008+
try
1009+
{
1010+
len += cipher.doFinal(buf, outOff + len);
1011+
}
1012+
catch (InvalidCipherTextException e)
1013+
{
1014+
test.fail("fail on testing split overlapping of " + label + ": " + e.getMessage());
1015+
}
1016+
test.isTrue("fail on testing split overlapping of " + label,
1017+
len == expected.length && Arrays.areEqual(expected, 0, expected.length, buf, outOff, outOff + len));
9451018
}
9461019
}

‎core/src/test/jdk1.3/org/bouncycastle/crypto/test/CipherTest.java‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,5 +949,78 @@ static void testOverlapping(SimpleTest test, int keySize, int ivSize, int macSiz
949949
test.isTrue("fail on testing overlapping of decryption for " + cipher.getAlgorithmName(),
950950
Arrays.areEqual(expected, 0, blockSize * 2, data, offset, offset + blockSize * 2));
951951

952+
testOverlappingSplit(test, keySize, ivSize, macSize, blockSize, cipher);
953+
}
954+
955+
/**
956+
* Encrypt and decrypt in two processBytes() calls with the input and the output in the same array, the
957+
* output starting at, behind or ahead of the input, and compare with a single call into a separate array.
958+
* The second call writes the bytes the first one buffered as well as its own, so its overlap check has to
959+
* allow for them.
960+
*/
961+
static void testOverlappingSplit(SimpleTest test, int keySize, int ivSize, int macSize, int blockSize, AEADCipher cipher)
962+
throws Exception
963+
{
964+
AEADParameters parameters = new AEADParameters(new KeyParameter(new byte[keySize]), macSize * 8, new byte[ivSize], null);
965+
int[] lags = new int[]{ -blockSize, 1 - blockSize, -1, 0, 1, blockSize };
966+
for (int dataLen = 2; dataLen <= blockSize * 3 + 2; dataLen++)
967+
{
968+
byte[] data = new byte[dataLen];
969+
for (int i = 0; i != dataLen; i++)
970+
{
971+
data[i] = (byte)i;
972+
}
973+
cipher.init(true, parameters);
974+
byte[] expected = new byte[cipher.getOutputSize(dataLen)];
975+
int len = cipher.processBytes(data, 0, dataLen, expected, 0);
976+
cipher.doFinal(expected, len);
977+
978+
for (int i = 0; i != lags.length; i++)
979+
{
980+
for (int split = 1; split < data.length; split++)
981+
{
982+
checkOverlappingSplit(test, true, parameters, data, expected, split, lags[i], blockSize + macSize, cipher);
983+
}
984+
for (int split = 1; split < expected.length; split++)
985+
{
986+
checkOverlappingSplit(test, false, parameters, expected, data, split, lags[i], blockSize + macSize, cipher);
987+
}
988+
}
989+
}
990+
}
991+
992+
private static void checkOverlappingSplit(SimpleTest test, boolean forEncryption, AEADParameters parameters, byte[] input,
993+
byte[] expected, int split, int lag, int margin, AEADCipher cipher)
994+
throws Exception
995+
{
996+
if (lag > 0)
997+
{
998+
// with the output ahead of the input, a first call returning more than split - lag bytes would
999+
// overwrite input the caller has not passed in yet, which is a caller error
1000+
cipher.init(forEncryption, parameters);
1001+
if (lag + cipher.processBytes(input, 0, split, new byte[input.length + margin], 0) > split)
1002+
{
1003+
return;
1004+
}
1005+
}
1006+
String label = (forEncryption ? "encryption" : "decryption") + " for " + cipher.getAlgorithmName()
1007+
+ ", length " + input.length + " split " + split + " lag " + lag;
1008+
int inOff = margin;
1009+
int outOff = inOff + lag;
1010+
byte[] buf = new byte[inOff + input.length + 2 * margin];
1011+
System.arraycopy(input, 0, buf, inOff, input.length);
1012+
cipher.init(forEncryption, parameters);
1013+
int len = cipher.processBytes(buf, inOff, split, buf, outOff);
1014+
len += cipher.processBytes(buf, inOff + split, input.length - split, buf, outOff + len);
1015+
try
1016+
{
1017+
len += cipher.doFinal(buf, outOff + len);
1018+
}
1019+
catch (InvalidCipherTextException e)
1020+
{
1021+
test.fail("fail on testing split overlapping of " + label + ": " + e.getMessage());
1022+
}
1023+
test.isTrue("fail on testing split overlapping of " + label,
1024+
len == expected.length && Arrays.areEqual(expected, 0, expected.length, buf, outOff, outOff + len));
9521025
}
9531026
}

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Date: 2026, TBD
6363
- The ML-KEM KeyGenerator (KEMGenerateSpec/KEMExtractSpec), Cipher (wrap/unwrap), javax.crypto.KEM and KeyFactory.translateKey services accepted only BC's own ML-KEM key objects, and the KeyGenerator failed with a ClassCastException at generateKey() rather than at init, so an ML-KEM key from another provider could not be used with BC even though its standard encoding was one BC reads. This broke BCJSSE handshakes over the ML-KEM and hybrid groups whenever another provider ahead of BC decoded the peer's key or generated the ephemeral key pair. A foreign key is now converted from its X.509 or PKCS#8 encoding, with the usual parameter-set checks, and an unusable one is rejected at init (github #2466).
6464
- The raw JCA provider bounded the PBKDF2 iteration count taken from an encoding (org.bouncycastle.pbe.max_iteration_count, default 10,000,000) but not the counts of the legacy PBES1 (PKCS#5 scheme 1) and PKCS#12 PBE families beside it. Their AlgorithmParameters (PKCS12PBE and its OID aliases, PBKDF1) accepted any count, narrowing one beyond the int range with intValue() so that 2^32 arrived as 0, and every Cipher, Mac and SecretKeyFactory derivation ran with whatever count it was given - including a count decoded by another provider's AlgorithmParameters, as when javax.crypto.EncryptedPrivateKeyInfo.getKeySpec() decrypts a PKCS#12 PBE-protected key with BC. As these schemes carry the count in unauthenticated parameters and derive before anything can be checked, a supplied blob could hold a derivation for tens of minutes. The parameter parse now rejects a negative, beyond-int or over-limit count, and the derivations reject a negative or over-limit count, under the same property as PBKDF2. The PKCS#12 key store derives through the same code, so a org.bouncycastle.pkcs12.max_it_count raised above 10,000,000 now needs org.bouncycastle.pbe.max_iteration_count raised with it.
6565
- The light-weight CryptoProWrapEngine (RFC 4357 sec. 6.3) diversified the key encryption key in the caller's own array, so after init the KeyParameter it was given held the diversified key, and initialising again with the same parameters - to unwrap what had just been wrapped, say - diversified it a second time and used a different key. It also failed with a NullPointerException when given no S-box, although init has a branch for that case. It now diversifies a copy, and given no S-box uses the GOST 28147 engine's default S-box for the diversification, the one the wrap itself then uses. The provider's GOST 28147 key wrap ciphers were unaffected, as they always supply an S-box and build a new KeyParameter on every init.
66+
- AsconAEAD128 and most of the other lightweight AEAD engines built on AEADBaseEngine (AsconEngine, Elephant, GIFT-COFB, ISAP, PhotonBeetle, Romulus-N and Romulus-T, Sparkle and Xoodyak) could corrupt data encrypted or decrypted in place, with the input and the output in the same array, when the data was passed in over more than one call. Before writing anything, processBytes() copies its input aside if the output it is about to write overlaps that input, but it sized that output from the length passed in alone, while the call also writes out the bytes an earlier call left buffered, so an overlap could go unnoticed and the engine wrote output over input it had not yet read. Depending on the engine, the chunk sizes and where the output started relative to the input, a valid ciphertext failed its tag check, or the engine produced a wrong ciphertext, which the receiver either rejected or, in some cases, accepted, getting the wrong plaintext with no error raised: Romulus-N, for one, could emit a ciphertext with an all-zero block that still authenticated. The overlap check now counts the buffered bytes as well. Grain-128AEAD and Romulus-M take other paths through the base class and were unaffected.
6667

6768
### 2.1.3 Additional Features and Functionality
6869

0 commit comments

Comments
 (0)