Skip to content

Commit c0911c2

Browse files
yazizaclaude
andauthored
[fix] Fix nonce/IV length validation for block cipher modes (#99)
Enhance security by enforcing proper nonce/IV lengths: - Block ciphers (CBC/CFB) now require nonce length equal to block size - Zero-length nonces rejected to prevent Botan's all-zero nonce fallback - OCB mode validates nonce length must be less than block size (1-15 bytes for AES) - Add comprehensive tests for nonce validation (zero-length, incorrect, correct) This prevents accidental use of insecure nonce configurations while maintaining backward compatibility with properly-sized all-zero IVs for legacy use cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Yasser Aziza <yasser.aziza@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2fc739e commit c0911c2

File tree

4 files changed

+181
-3
lines changed

4 files changed

+181
-3
lines changed

src/main/java/net/randombit/botan/seckey/block/BotanBlockCipher.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,20 @@ private boolean isWithoutPadding() {
193193
return PaddingAlgorithm.NO_PADDING == padding;
194194
}
195195

196+
/**
197+
* Validates the nonce/IV length for block cipher modes.
198+
*
199+
* <p>Zero-length nonce are not allowed since Botan will interpret them as all-zero nonce,
200+
* which is a security risk for CBC/CFB modes. If an all-zero nonce is needed for legacy
201+
* reasons, use a properly-sized byte array filled with zeros (e.g., {@code byte[16]} for AES)
202+
* instead of a zero-length array.
203+
*
204+
* @param nonceLength the length of the nonce/IV in bytes
205+
* @return {@code true} if the nonce length is valid, {@code false} otherwise
206+
*/
196207
@Override
197208
protected boolean isValidNonceLength(int nonceLength) {
198-
return true;
209+
return nonceLength == engineGetBlockSize();
199210
}
200211

201212
/** AES-CBC cipher implementation. */

src/main/java/net/randombit/botan/seckey/block/aead/BotanAeadCipher.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ protected java.security.AlgorithmParameters engineGetParameters() {
7272
parameters.init(new AeadParameterSpec(tLen, iv));
7373

7474
} catch (java.security.NoSuchAlgorithmException
75-
| java.security.spec.InvalidParameterSpecException e) {
75+
| java.security.spec.InvalidParameterSpecException e) {
7676
parameters = null;
7777
}
7878
}
@@ -356,7 +356,14 @@ protected String getBotanCipherName(int keySize) {
356356

357357
@Override
358358
protected boolean isValidNonceLength(int nonceLength) {
359-
return nonceLength != 0 && nonceLength < 16;
359+
if (nonceLength == 0) {
360+
return false;
361+
}
362+
if (engineGetBlockSize() == 16) {
363+
return nonceLength < 16;
364+
} else {
365+
return nonceLength < (engineGetBlockSize() - 1);
366+
}
360367
}
361368

362369
@Override

src/test/java/net/randombit/botan/seckey/block/BotanBlockCipherTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,4 +795,81 @@ void testUpdateWithOffsetDoesNotCopyExtraData() throws Exception {
795795

796796
LOG.info("SUCCESS: update correctly processes only inputLen bytes even with buffered data");
797797
}
798+
799+
@ParameterizedTest
800+
@CsvFileSource(
801+
resources = {"/seckey/block/cbc_no_padding.csv", "/seckey/block/cfb_no_padding.csv"},
802+
numLinesToSkip = 1)
803+
@DisplayName("Test zero-length nonce is rejected")
804+
public void testZeroLengthNonceRejected(String algorithm, int blockSize, int keySize)
805+
throws GeneralSecurityException {
806+
final Cipher cipher = Cipher.getInstance(algorithm, BotanProvider.NAME);
807+
final SecretKeySpec key = new SecretKeySpec(new byte[keySize], algorithm);
808+
final IvParameterSpec zeroLengthIv = new IvParameterSpec(new byte[0]);
809+
810+
// Extract base cipher name (e.g., "AES" from "AES/CBC/NoPadding")
811+
String baseCipherName = algorithm.substring(0, algorithm.indexOf('/'));
812+
813+
final Exception exception =
814+
assertThrows(
815+
java.security.InvalidAlgorithmParameterException.class,
816+
() -> cipher.init(Cipher.ENCRYPT_MODE, key, zeroLengthIv));
817+
818+
assertEquals(
819+
String.format("Nonce with length 0 not allowed for algorithm %s", baseCipherName),
820+
exception.getMessage(),
821+
"Zero-length nonce should be rejected for " + algorithm);
822+
}
823+
824+
@ParameterizedTest
825+
@CsvFileSource(
826+
resources = {"/seckey/block/cbc_no_padding.csv", "/seckey/block/cfb_no_padding.csv"},
827+
numLinesToSkip = 1)
828+
@DisplayName("Test incorrect nonce length is rejected")
829+
public void testIncorrectNonceLengthRejected(String algorithm, int blockSize, int keySize)
830+
throws GeneralSecurityException {
831+
final Cipher cipher = Cipher.getInstance(algorithm, BotanProvider.NAME);
832+
final SecretKeySpec key = new SecretKeySpec(new byte[keySize], algorithm);
833+
834+
// Use wrong nonce length (blockSize + 1 instead of blockSize)
835+
final IvParameterSpec wrongLengthIv = new IvParameterSpec(new byte[blockSize + 1]);
836+
837+
// Extract base cipher name (e.g., "AES" from "AES/CBC/NoPadding")
838+
String baseCipherName = algorithm.substring(0, algorithm.indexOf('/'));
839+
840+
final Exception exception =
841+
assertThrows(
842+
java.security.InvalidAlgorithmParameterException.class,
843+
() -> cipher.init(Cipher.ENCRYPT_MODE, key, wrongLengthIv));
844+
845+
assertEquals(
846+
String.format(
847+
"Nonce with length %d not allowed for algorithm %s", blockSize + 1, baseCipherName),
848+
exception.getMessage(),
849+
"Incorrect nonce length should be rejected for " + algorithm);
850+
}
851+
852+
@ParameterizedTest
853+
@CsvFileSource(
854+
resources = {"/seckey/block/cbc_no_padding.csv", "/seckey/block/cfb_no_padding.csv"},
855+
numLinesToSkip = 1)
856+
@DisplayName("Test correct nonce length is accepted")
857+
public void testCorrectNonceLengthAccepted(String algorithm, int blockSize, int keySize)
858+
throws GeneralSecurityException {
859+
final Cipher cipher = Cipher.getInstance(algorithm, BotanProvider.NAME);
860+
final SecretKeySpec key = new SecretKeySpec(new byte[keySize], algorithm);
861+
final IvParameterSpec correctLengthIv = new IvParameterSpec(new byte[blockSize]);
862+
863+
// This should not throw an exception
864+
cipher.init(Cipher.ENCRYPT_MODE, key, correctLengthIv);
865+
866+
// Verify cipher is properly initialized by encrypting some data
867+
byte[] plaintext = new byte[blockSize];
868+
byte[] ciphertext = cipher.doFinal(plaintext);
869+
870+
assertEquals(
871+
blockSize,
872+
ciphertext.length,
873+
"Encryption should succeed with correct nonce length for " + algorithm);
874+
}
798875
}

src/test/java/net/randombit/botan/seckey/block/aead/BotanAeadCipherTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,4 +1302,87 @@ public void testAeadParameterSpecAADMismatch() throws GeneralSecurityException {
13021302
"Decryption should fail with wrong AAD");
13031303
LOG.info("SUCCESS: AAD mismatch properly detected");
13041304
}
1305+
1306+
@Test
1307+
@DisplayName("Test OCB zero-length nonce is rejected")
1308+
public void testOcbZeroLengthNonceRejected() throws GeneralSecurityException {
1309+
final Cipher cipher = Cipher.getInstance("AES/OCB/NoPadding", BotanProvider.NAME);
1310+
final SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
1311+
final AeadParameterSpec zeroLengthNonce = new AeadParameterSpec(128, new byte[0]);
1312+
1313+
final Exception exception =
1314+
assertThrows(
1315+
IllegalArgumentException.class,
1316+
() -> cipher.init(Cipher.ENCRYPT_MODE, key, zeroLengthNonce));
1317+
1318+
assertEquals(
1319+
"Nonce with length 0 not allowed for algorithm AES",
1320+
exception.getMessage(),
1321+
"Zero-length nonce should be rejected for OCB");
1322+
}
1323+
1324+
@Test
1325+
@DisplayName("Test OCB nonce length equal to block size is rejected")
1326+
public void testOcbBlockSizeNonceRejected() throws GeneralSecurityException {
1327+
final Cipher cipher = Cipher.getInstance("AES/OCB/NoPadding", BotanProvider.NAME);
1328+
final SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
1329+
// OCB requires nonce < block size, so 16-byte nonce should be rejected for AES
1330+
final AeadParameterSpec blockSizeNonce = new AeadParameterSpec(128, new byte[16]);
1331+
1332+
final Exception exception =
1333+
assertThrows(
1334+
IllegalArgumentException.class,
1335+
() -> cipher.init(Cipher.ENCRYPT_MODE, key, blockSizeNonce));
1336+
1337+
assertEquals(
1338+
"Nonce with length 16 not allowed for algorithm AES",
1339+
exception.getMessage(),
1340+
"Nonce length equal to block size should be rejected for OCB");
1341+
}
1342+
1343+
@Test
1344+
@DisplayName("Test OCB nonce length greater than block size is rejected")
1345+
public void testOcbTooLongNonceRejected() throws GeneralSecurityException {
1346+
final Cipher cipher = Cipher.getInstance("AES/OCB/NoPadding", BotanProvider.NAME);
1347+
final SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
1348+
// OCB requires nonce < block size, so 20-byte nonce should be rejected for AES
1349+
final AeadParameterSpec tooLongNonce = new AeadParameterSpec(128, new byte[20]);
1350+
1351+
final Exception exception =
1352+
assertThrows(
1353+
IllegalArgumentException.class,
1354+
() -> cipher.init(Cipher.ENCRYPT_MODE, key, tooLongNonce));
1355+
1356+
assertEquals(
1357+
"Nonce with length 20 not allowed for algorithm AES",
1358+
exception.getMessage(),
1359+
"Nonce length greater than block size should be rejected for OCB");
1360+
}
1361+
1362+
@Test
1363+
@DisplayName("Test OCB valid nonce lengths are accepted")
1364+
public void testOcbValidNonceLengthsAccepted() throws GeneralSecurityException {
1365+
final Cipher cipher = Cipher.getInstance("AES/OCB/NoPadding", BotanProvider.NAME);
1366+
final SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
1367+
1368+
// Test various valid nonce lengths (1-15 bytes for AES with 16-byte block size)
1369+
int[] validLengths = {1, 8, 12, 15};
1370+
1371+
for (int length : validLengths) {
1372+
final AeadParameterSpec validNonce = new AeadParameterSpec(128, new byte[length]);
1373+
1374+
// This should not throw an exception
1375+
cipher.init(Cipher.ENCRYPT_MODE, key, validNonce);
1376+
1377+
// Verify cipher is properly initialized by encrypting some data
1378+
byte[] plaintext = "Test message".getBytes();
1379+
byte[] ciphertext = cipher.doFinal(plaintext);
1380+
1381+
// Ciphertext should be plaintext + tag (16 bytes for 128-bit tag)
1382+
assertEquals(
1383+
plaintext.length + 16,
1384+
ciphertext.length,
1385+
"Encryption should succeed with nonce length " + length + " for OCB");
1386+
}
1387+
}
13051388
}

0 commit comments

Comments
 (0)