It’s fairly standard knowledge that due to the cryptography export controls, Oracle JRE ships with “limited” cryptographic strength enabled as listed in the JCA Documentation. For AES, the default max happens to be 128 bit
key length. To enable 192 bit
or 256 bit
encryption, the JCE Unlimited Strength Jurisdiction Policy files must be installed into the JRE.
I came across a situation by accident recently that lead me to believe there is an issue with this enforcement. I’m not sure I would call it a bug, but it’s definitely not well documented (or at least I can’t find anything documenting it).
The key length check is done inside cipher.init()
, and I believe it uses Cipher.getMaxAllowedKeyLength("AES")
to determine if the max key size is 128
or Integer.MAX_VALUE
.
Using normal keyed encryption, this check is fine. On a default JRE installation, the code below executes as expected (I’m using Groovy for the test but I’ve tried this in pure Java as well):
static boolean isUnlimitedStrengthCrypto() {
Cipher.getMaxAllowedKeyLength("AES") > 128
}
@Test
public void testShouldEncryptAndDecryptWith128BitKey() throws Exception {
// Arrange
MessageDigest sha1 = MessageDigest.getInstance("SHA1")
String key = Hex.encodeHexString(sha1.digest("thisIsABadPassword".getBytes()))[0..<32]
String iv = Hex.encodeHexString(sha1.digest("thisIsABadIv".getBytes()))[0..<32]
logger.info("Key: ${key}")
logger.info("IV : ${iv}")
SecretKey secretKey = new SecretKeySpec(Hex.decodeHex(key.toCharArray()), "AES")
IvParameterSpec ivParameterSpec = new IvParameterSpec(Hex.decodeHex(iv.toCharArray()))
// Act
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
String message = "This is a plaintext message."
byte[] cipherBytes = cipher.doFinal(message.getBytes())
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes)
System.out.println("Recovered message: " + recovered)
// Assert
assert recovered == message
}
This generates the output:
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - Key: 6d71f677ecb99cf623246fb48a1d8130
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - IV : 912ed675905eb4cb0f9f5714c9c9ec39
And this test:
@Test
public void testShouldNotEncryptAndDecryptWith256BitKey() throws Exception {
// Arrange
Assume.assumeTrue("This test should only run when unlimited (256 bit) encryption is not available", !isUnlimitedStrengthCrypto())
MessageDigest sha1 = MessageDigest.getInstance("SHA1")
String key = Hex.encodeHexString(sha1.digest("thisIsABadPassword".getBytes()))[0..<32] * 2
String iv = Hex.encodeHexString(sha1.digest("thisIsABadIv".getBytes()))[0..<32]
logger.info("Key: ${key}")
logger.info("IV : ${iv}")
SecretKey secretKey = new SecretKeySpec(Hex.decodeHex(key.toCharArray()), "AES")
IvParameterSpec ivParameterSpec = new IvParameterSpec(Hex.decodeHex(iv.toCharArray()))
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
// Act
def msg = shouldFail(InvalidKeyException) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
}
// Assert
assert msg =~ "Illegal key size"
}
Generates this output:
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - Key: 6d71f677ecb99cf623246fb48a1d81306d71f677ecb99cf623246fb48a1d8130
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - IV : 912ed675905eb4cb0f9f5714c9c9ec39
And successfully passes by throwing the exception.
The problem arises when password-based encryption is used.
Because the key derivation from the password (and salt, if provided), occurs during cipher.init()
but after the key length check, the length check actually applies to the byte[]
representation of SecretKey.getEncoded()
. This means that if a password <= 16 characters (16 bytes / 128 bits
) is used, the check will pass even if the cipher specified uses a 256 bit
key. The derived key will be 256 bits
even though the jurisdiction policy prohibits this. Conversely, if a password > 16 characters is used, even with a 128 bit
cipher, the length check will fail and an InvalidKeyException
will be thrown. The following code demonstrates this:
@Test
public void testShouldEncryptAndDecryptWithPBEShortPassword() throws Exception {
// Arrange
final String PASSWORD = "password"
String salt = "saltsalt"
logger.info("Password: ${PASSWORD}")
logger.info("Salt : ${salt}")
String algorithm;
algorithm = "PBEWITHMD5AND256BITAES-CBC-OPENSSL"
PBEKeySpec pbeSpec = new PBEKeySpec(PASSWORD.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm, "BC");
SecretKey secretKey = secretKeyFactory.generateSecret(pbeSpec);
PBEParameterSpec saltParams = new PBEParameterSpec(salt.getBytes("US-ASCII"), 0);
// Act
Cipher cipher = Cipher.getInstance(algorithm, "BC");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, saltParams);
String message = "This is a plaintext message."
byte[] cipherBytes = cipher.doFinal(message.getBytes())
cipher.init(Cipher.DECRYPT_MODE, secretKey, saltParams)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes)
System.out.println("Recovered message: " + recovered)
// Assert
assert recovered == message
}
@Test
public void testShouldNotEncryptAndDecryptWithPBELongPassword() throws Exception {
// Arrange
Assume.assumeTrue("This test should only run when unlimited (256 bit) encryption is not available", !isUnlimitedStrengthCrypto())
final String PASSWORD = "thisIsABadPassword"
String salt = "saltsalt"
logger.info("Password: ${PASSWORD}")
logger.info("Salt : ${salt}")
String algorithm;
algorithm = "PBEWITHMD5AND256BITAES-CBC-OPENSSL"
PBEKeySpec pbeSpec = new PBEKeySpec(PASSWORD.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm, "BC");
SecretKey secretKey = secretKeyFactory.generateSecret(pbeSpec);
PBEParameterSpec saltParams = new PBEParameterSpec(salt.getBytes("US-ASCII"), 0);
Cipher cipher = Cipher.getInstance(algorithm, "BC");
// Act
def msg = shouldFail(InvalidKeyException) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, saltParams);
}
// Assert
assert msg =~ "Illegal key size"
}
Both tests “pass” in that on a system with “limited” strength cryptography, 256 bit
encryption is still available if the password is short enough. Conversely, this test demonstrates that a long password causes an exception even when using 128 bit
encryption:
@Test
public void testShouldNotEncryptAndDecryptWithPBELongPasswordEvenWith128BitKey() throws Exception {
// Arrange
Assume.assumeTrue("This test should only run when unlimited (256 bit) encryption is not available", !isUnlimitedStrengthCrypto())
final String PASSWORD = "thisIsABadPassword"
String salt = "saltsalt"
logger.info("Password: ${PASSWORD}")
logger.info("Salt : ${salt}")
String algorithm;
algorithm = "PBEWITHMD5AND128BITAES-CBC-OPENSSL"
PBEKeySpec pbeSpec = new PBEKeySpec(PASSWORD.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm, "BC");
SecretKey secretKey = secretKeyFactory.generateSecret(pbeSpec);
PBEParameterSpec saltParams = new PBEParameterSpec(salt.getBytes("US-ASCII"), 0);
Cipher cipher = Cipher.getInstance(algorithm, "BC");
// Act
def msg = shouldFail(InvalidKeyException) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, saltParams);
}
// Assert
assert msg =~ "Illegal key size"
}
I thought it might be a false positive, so I used OpenSSL to encrypt files using 128
and 256 bit
encryption with “long” and “short” passwords and tried to decrypt them with Java. The results are from a system with “limited” strength crypto:
$ openssl enc -aes-128-cbc -e -in plain.txt -out salted_raw_128_long.enc -k thisIsABadPassword -p
$ openssl enc -aes-128-cbc -e -in plain.txt -out salted_raw_128_short.enc -k password -p
$ openssl enc -aes-256-cbc -e -in plain.txt -out salted_raw_256_long.enc -k thisIsABadPassword -p
$ openssl enc -aes-256-cbc -e -in plain.txt -out salted_raw_256_short.enc -k password -p
Cipher | Password length | Should Work | Does Work
--------|-----------------|-------------|-----------
AES-128 | <= 16 chars | YES | YES
AES-128 | > 16 chars | YES | NO
AES-256 | <= 16 chars | NO | YES
AES-256 | > 16 chars | NO | NO
I have a few questions:
- Can anyone else reproduce this behavior?
- Is this intended behavior or a bug?
- If intended, is it sufficiently documented somewhere?
Update After further research on a machine without the unlimited strength jurisdiction policies installed, I have determined these maximum password lengths for the following PBE algorithms:
Algorithm | Max Password Length
---------------------------------------------
PBEWITHMD5AND128BITAES-CBC-OPENSSL | 16
PBEWITHMD5AND192BITAES-CBC-OPENSSL | 16
PBEWITHMD5AND256BITAES-CBC-OPENSSL | 16
PBEWITHMD5ANDDES | 16
PBEWITHMD5ANDRC2 | 16
PBEWITHSHA1ANDRC2 | 16
PBEWITHSHA1ANDDES | 16
PBEWITHSHAAND128BITAES-CBC-BC | 7
PBEWITHSHAAND192BITAES-CBC-BC | 7
PBEWITHSHAAND256BITAES-CBC-BC | 7
PBEWITHSHAAND40BITRC2-CBC | 7
PBEWITHSHAAND128BITRC2-CBC | 7
PBEWITHSHAAND40BITRC4 | 7
PBEWITHSHAAND128BITRC4 | 7
PBEWITHSHA256AND128BITAES-CBC-BC | 7
PBEWITHSHA256AND192BITAES-CBC-BC | 7
PBEWITHSHA256AND256BITAES-CBC-BC | 7
PBEWITHSHAAND2-KEYTRIPLEDES-CBC | 7
PBEWITHSHAAND3-KEYTRIPLEDES-CBC | 7
PBEWITHSHAANDTWOFISH-CBC | 7
Continue reading Why does Java allow AES-256 bit encryption on systems without JCE unlimited strength policies if using PBE?→