È risaputo che grazie ai controlli di esportazione della crittografia, Oracle JRE viene fornito con resistenza crittografica "limitata" abilitata come elencata in Documentazione JCA . Per AES, il valore predefinito massimo corrisponde alla lunghezza della chiave 128 bit
. Per attivare la crittografia 192 bit
o 256 bit
, i file delle JCE Unlimited Strength Jurisdiction Policy deve essere installato nel JRE.
Recentemente mi sono imbattuto in una situazione per caso che mi ha portato a credere che ci sia un problema con questa applicazione. Non sono sicuro che lo chiamerei un bug, ma sicuramente non è ben documentato (o almeno non riesco a trovare nulla che lo documenta).
Il controllo della lunghezza della chiave viene eseguito all'interno di cipher.init()
, e credo che utilizzi Cipher.getMaxAllowedKeyLength("AES")
per determinare se la dimensione massima della chiave è 128
o Integer.MAX_VALUE
.
Usando la normale crittografia con chiave, questo controllo va bene. Su un'installazione JRE predefinita, il codice sottostante viene eseguito come previsto (sto utilizzando Groovy per il test ma l'ho provato anche in puro Java):
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
}
Questo genera l'output:
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - Key: 6d71f677ecb99cf623246fb48a1d8130
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - IV : 912ed675905eb4cb0f9f5714c9c9ec39
E questo 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"
}
Genera questo output:
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - Key: 6d71f677ecb99cf623246fb48a1d81306d71f677ecb99cf623246fb48a1d8130
[main] INFO *.crypto.OpenSSLPBEEncryptorTest - IV : 912ed675905eb4cb0f9f5714c9c9ec39
E passa con successo lanciando l'eccezione.
Il problema sorge quando viene utilizzata la crittografia basata su password.
Poiché la derivazione della chiave dalla password (e salt, se fornita), si verifica durante cipher.init()
ma dopo il controllo della lunghezza della chiave, il controllo della lunghezza si applica effettivamente alla rappresentazione byte[]
di% codice%. Ciò significa che se viene utilizzata una password < = 16 caratteri ( SecretKey.getEncoded()
), il controllo passerà anche se la cifra specificata utilizza una chiave 16 bytes / 128 bits
. La chiave derivata sarà 256 bit
anche se la politica di giurisdizione lo proibisce. Al contrario, se una password > 16 caratteri sono usati, anche con un 256 bits
cipher, il controllo della lunghezza fallirà e verrà lanciato un 128 bit
. Il seguente codice lo dimostra:
@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"
}
Entrambi i test "passano" in quello su un sistema con crittografia a forza "limitata", la crittografia InvalidKeyException
è ancora disponibile se la password è abbastanza breve. Al contrario, questo test dimostra che una password lunga causa un'eccezione anche quando si utilizza la crittografia 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"
}
Ho pensato che potesse essere un falso positivo, quindi ho usato OpenSSL per crittografare i file usando 128 bit
e 128
crittografia con password "lunghe" e "brevi" e ho provato a decodificarle con Java. I risultati provengono da un sistema con crittografia di forza "limitata":
$ 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
Ho alcune domande:
- Qualcun altro può riprodurre questo comportamento?
- Questo comportamento è previsto o un bug?
- Se previsto, è sufficientemente documentato da qualche parte?
Aggiornamento Dopo ulteriori ricerche su una macchina senza le policy di giurisdizione illimitate, ho determinato queste lunghezze massime della password per i seguenti algoritmi 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