Es un conocimiento bastante estándar que, debido a los controles de exportación de criptografía, Oracle JRE se envía con la fuerza criptográfica "limitada" habilitada como se indica en Documentación JCA . Para AES, el max predeterminado es la longitud de la clave 128 bit
. Para habilitar el cifrado 192 bit
o 256 bit
, JCE archivos de política de jurisdicción de jurisdicción ilimitada debe estar instalado en el JRE.
Hace poco me encontré con una situación por accidente que me llevó a creer que hay un problema con esta aplicación. No estoy seguro de llamarlo error, pero definitivamente no está bien documentado (o al menos no puedo encontrar nada que lo documente).
La verificación de la longitud de la clave se realiza dentro de cipher.init()
, y creo que usa Cipher.getMaxAllowedKeyLength("AES")
para determinar si el tamaño máximo de la clave es 128
o Integer.MAX_VALUE
.
Al utilizar el cifrado con clave normal, esta comprobación está bien. En una instalación de JRE predeterminada, el código siguiente se ejecuta como se esperaba (estoy usando Groovy para la prueba, pero también he intentado esto en Java puro):
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
}
Esto genera la salida:
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - Key: 6d71f677ecb99cf623246fb48a1d8130
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - IV : 912ed675905eb4cb0f9f5714c9c9ec39
Y esta prueba:
@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"
}
Genera esta salida:
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - Key: 6d71f677ecb99cf623246fb48a1d81306d71f677ecb99cf623246fb48a1d8130
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - IV : 912ed675905eb4cb0f9f5714c9c9ec39
Y pasa con éxito lanzando la excepción.
El problema surge cuando se utiliza el cifrado basado en contraseña.
Debido a que la derivación de la clave de la contraseña (y sal, si se proporciona), ocurre durante cipher.init()
pero después de la verificación de la longitud de la clave, la verificación de la longitud en realidad se aplica a la representación byte[]
de% código%. Esto significa que si se usa una contraseña < = 16 caracteres ( SecretKey.getEncoded()
), la verificación pasará incluso si el cifrado especificado usa una clave 16 bytes / 128 bits
. La clave derivada será 256 bit
aunque la política de jurisdicción lo prohíba. A la inversa, si una contraseña > Se utilizan 16 caracteres, incluso con un cifrado 256 bits
, la verificación de longitud fallará y se lanzará un 128 bit
. El siguiente código demuestra esto:
@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"
}
Ambas pruebas "pasan" en eso en un sistema con criptografía de fuerza "limitada", el cifrado InvalidKeyException
todavía está disponible si la contraseña es lo suficientemente corta. A la inversa, esta prueba demuestra que una contraseña larga causa una excepción incluso cuando se utiliza el cifrado 256 bit
:
@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"
}
Pensé que podría ser un falso positivo, así que utilicé OpenSSL para cifrar archivos usando el cifrado 128 bit
y 128
con contraseñas "largas" y "cortas" y traté de descifrarlos con Java. Los resultados son de un sistema con criptografía de fuerza "limitada":
$ 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
Tengo algunas preguntas:
- ¿Alguien más puede reproducir este comportamiento?
- ¿Es este un comportamiento intencionado o un error?
- Si está destinado, ¿está suficientemente documentado en alguna parte?
Actualizar Después de investigar más en una máquina sin las políticas de jurisdicción de fuerza ilimitada instaladas, he determinado estas longitudes máximas de contraseña para los siguientes algoritmos PBE:
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