Java equivalente de un cifrado AES CBC OpenSSL

No soy un experto en criptografía y especialmente debido al hecho de que OpenSSL tiene mucha documentación faltante, no estoy seguro de cómo puedo resolver este problema.

Tengo un sistema externo que espera recibir mensajes encriptados. El único ejemplo proporcionado utiliza OpenSSL de esta manera:

$ openssl enc -aes-256-cbc -a -in t.txt -k testpass U2FsdGVkX1/RUdaSJKRXhHv3zUyTsQwu5/ar2ECKDlrNyH5GL4xRR4fgxkiWqkS1 cQstcoSIgWfRPSOFj/5OtdNLeNXiVR6MxSKJ+NvS9LyUD8+Rg6XIcYUvxR4gHi3w DWT44LAMCpRAh1Q0t4Z2g7rwb0D05T6ygLaWvB5zD/xGZD3brTqSlWmiJb9Imgda M6soZO7BhbYdqWqEUl5r6+EbkD21f6L3NX3hJFo+BJ+VFctiAlBO8NwT5l4ogo/s GErm8gqRr57XoX/kvKAimg== 

Donde el archivo t.txt contiene esta cadena en una línea:

 AMOUNT=10&TID=#19:23&CURRENCY=EUR&LANGUAGE=DE&SUCCESS_URL=http://some.url/sucess&ERROR_URL=http://some.url/error&CONFIRMATION_URL=http://some.url/confirm&NAME=customer full name` 

He encontrado esta otra pregunta y he podido hacer la encriptación usando el siguiente código:

 String password = "passPhrase"; String salt = "15charRandomSalt"; int iterations = 100; /* Derive the key, given password and salt. */ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(Charset.forName("UTF8")), iterations, 256); SecretKey tmp = factory.generateSecret(spec); SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); /* Encrypt the message. */ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secret); AlgorithmParameters params = cipher.getParameters(); byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); byte[] cipherText = cipher.doFinal(toBeEncrypted.getBytes("UTF-8")); encryptedData = Base64.getEncoder().encodeToString(cipherText); encryptedData += Base64.getEncoder().encodeToString(iv); 

Lo que no puedo entender es cómo debería generar un resultado similar (encryptedData) a lo que hace OpenSSL. Tengo el sal, iv y cipherText, ¿es el resultado codificado Base64 de salida OpenSSL de una concatenación de estos? o solo uno de ellos?

Lo único que comparto con ese otro sistema antes del cifrado es la frase de contraseña. ¿Cómo podrían descifrar el resultado si no conocen la sal y el número de iteraciones?

¿Alguien puede dar respuestas a esos parámetros desconocidos y también decirme si el código anterior es equivalente al proceso de OpenSSL?

A continuación se muestra un progtwig Java para descifrar el cifrado OPENSSL anterior (requiere Java 8):

 import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.MessageDigest; import java.util.Arrays; import java.util.Base64; import java.util.Base64.Decoder; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class TestAesDecrypt { public static void main(final String[] args) throws Exception { final byte[] pass = "testpass".getBytes(StandardCharsets.US_ASCII); final byte[] magic = "Salted__".getBytes(StandardCharsets.US_ASCII); final String inFile = "e:/t/e.txt"; String source = new String(Files.readAllBytes(Paths.get(inFile)), StandardCharsets.US_ASCII); source = source.replaceAll("\\s", ""); final Decoder decoder = Base64.getDecoder(); final byte[] inBytes = decoder.decode(source); final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes, 0, magic.length); if (!Arrays.equals(shouldBeMagic, magic)) { System.out.println("Bad magic number"); return; } final byte[] salt = Arrays.copyOfRange(inBytes, magic.length, magic.length + 8); final byte[] passAndSalt = concat(pass, salt); byte[] hash = new byte[0]; byte[] keyAndIv = new byte[0]; for (int i = 0; i < 3; i++) { final byte[] data = concat(hash, passAndSalt); final MessageDigest md = MessageDigest.getInstance("MD5"); hash = md.digest(data); keyAndIv = concat(keyAndIv, hash); } final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32); final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48); final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); final SecretKeySpec key = new SecretKeySpec(keyValue, "AES"); cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); final byte[] clear = cipher.doFinal(inBytes, 16, inBytes.length - 16); final String clearText = new String(clear, StandardCharsets.ISO_8859_1); System.out.println(clearText); } private static byte[] concat(final byte[] a, final byte[] b) { final byte[] c = new byte[a.length + b.length]; System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(b, 0, c, a.length, b.length); return c; } } 

Esta pregunta tiene una respuesta aceptada que es un poco antigua, sin embargo, esto parece ser algo que surge una y otra vez. Tengo 2 proyectos si nos comunicamos con terceros y el cifrado es OpenSSL AES con una clave precompartida.

He usado la biblioteca no-común-ssl. Sin embargo, parece estar estancado en la versión 0.3.xy sin lanzamientos en casi 2 años, no hay tráfico de listas de correo o desarrollo visible, tengo que concluir que esto está esencialmente muerto.

En función de algunas preguntas adicionales sobre el stackover, encontré tanto Spring Security como Encryptor4j, que parecen ofrecer una encoding de texto razonablemente empaquetada. Sin embargo, al intentar hacer que los Encryptors de Spring Security funcionaran para decodificar una cadena de texto codificada conocida, creo que la generación IV y Key utilizada por OpenSSL simplemente no es compatible con la implementación suministrada.

Al examinar el código anterior, así como una implementación conocida de C # y PHP, pude encontrar una clase de utilidad que actualmente está pasando mis pruebas de interoperabilidad. En general, preferiría usar una biblioteca conocida, pero si hay una, no he podido localizarla. La clase ( https://gist.github.com/rrsIPOV/4d0f6be7c58173c16e9edf9f97c7d7f2 ) es la siguiente:

 import groovy.transform.CompileStatic; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.security.SecureRandom; import static java.nio.charset.StandardCharsets.* /** * Mimics the OpenSSL AES Cipher options for encrypting and decrypting messages using a shared key (aka password) with symetric ciphers. */ @CompileStatic class OpenSslAes { /** OpenSSL's magic initial bytes. */ private static final String SALTED_STR = "Salted__"; private static final byte[] SALTED_MAGIC = SALTED_STR.getBytes(US_ASCII); static String encryptAndURLEncode(String password, String clearText) { String encrypted = encrypt(password, clearText); return URLEncoder.encode(encrypted, UTF_8.name() ); } /** * * @param password The password / key to encrypt with. * @param data The data to encrypt * @return A base64 encoded string containing the encrypted data. */ static String encrypt(String password, String clearText) { final byte[] pass = password.getBytes(US_ASCII); final byte[] salt = (new SecureRandom()).generateSeed(8); final byte[] inBytes = clearText.getBytes(UTF_8); final byte[] passAndSalt = array_concat(pass, salt); byte[] hash = new byte[0]; byte[] keyAndIv = new byte[0]; for (int i = 0; i < 3 && keyAndIv.length < 48; i++) { final byte[] hashData = array_concat(hash, passAndSalt); final MessageDigest md = MessageDigest.getInstance("MD5"); hash = md.digest(hashData); keyAndIv = array_concat(keyAndIv, hash); } final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32); final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48); final SecretKeySpec key = new SecretKeySpec(keyValue, "AES"); final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); byte[] data = cipher.doFinal(inBytes); data = array_concat(array_concat(SALTED_MAGIC, salt), data); return Base64.getEncoder().encodeToString( data ); } /** * @see http://stackoverflow.com/questions/32508961/java-equivalent-of-an-openssl-aes-cbc-encryption for what looks like a useful answer. The not-yet-commons-ssl also has an implementation * @param password * @param source The encrypted data * @return */ static String decrypt(String password, String source) { final byte[] pass = password.getBytes(US_ASCII); final byte[] inBytes = Base64.getDecoder().decode(source); final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes, 0, SALTED_MAGIC.length); if (!Arrays.equals(shouldBeMagic, SALTED_MAGIC)) { throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value."); } final byte[] salt = Arrays.copyOfRange(inBytes, SALTED_MAGIC.length, SALTED_MAGIC.length + 8); final byte[] passAndSalt = array_concat(pass, salt); byte[] hash = new byte[0]; byte[] keyAndIv = new byte[0]; for (int i = 0; i < 3 && keyAndIv.length < 48; i++) { final byte[] hashData = array_concat(hash, passAndSalt); final MessageDigest md = MessageDigest.getInstance("MD5"); hash = md.digest(hashData); keyAndIv = array_concat(keyAndIv, hash); } final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32); final SecretKeySpec key = new SecretKeySpec(keyValue, "AES"); final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48); final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); final byte[] clear = cipher.doFinal(inBytes, 16, inBytes.length - 16); return new String(clear, UTF_8); } private static byte[] array_concat(final byte[] a, final byte[] b) { final byte[] c = new byte[a.length + b.length]; System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(b, 0, c, a.length, b.length); return c; } } 

Puede ver esta discusión especificando el algoritmo de generación de claves como la concatenación de dos hashes MD5.

En cuanto a la sal mencionada allí, la página del hombre de opensssl enc dice:

Cuando se utiliza la sal, los primeros ocho bytes de los datos cifrados se reservan para la sal: se generan aleatoriamente al cifrar un archivo y leer desde el archivo cifrado cuando se descifra.

En este momento, la versión 1.1.0f-3 de openssl requiere una función de resumen SHA-256. Sin esto, no puede decodificar.