Java-эквивалент шифрования OpenSSL AES CBC
Я не криптография profi и специально из-за того, что OpenSSL имеет много недостающей документации, я не уверен, как решить эту проблему.
У меня есть внешняя система, которая рассчитывает получить зашифрованные сообщения. В приведенном примере используется OpenSSL таким образом:
$ 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==
Если файл t.txt
содержит эту строку в одной строке:
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`
Я нашел этот другой вопрос, и я смог выполнить шифрование, используя следующий код:
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);
Что я не могу понять, так это то, как я должен сгенерировать аналогичный вывод (encryptedData) для того, что делает OpenSSL. У меня есть соль, iv и cipherText, это результат OpenSSL, полученный в кодировке Base64 в результате их конкатенации? или только один из них?
Единственное, что я разделяю с этой другой системой перед шифрованием, это фраза. Как они могут расшифровать результат, если им не известны соли и количество итераций?
Может кто-нибудь дать ответы на эти неизвестные параметры, а также сказать, является ли приведенный выше код эквивалентом процесса OpenSSL?
Ответы
Ответ 1
Ниже приведена программа Java для расшифровки вышеописанного шифрования OPENSSL (для этого требуется 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;
}
}
Ответ 2
У этого вопроса есть принятый ответ, который немного устарел, однако это похоже на то, что появляется снова и снова. У меня есть 2 проекта, мы общались с третьими сторонами, а шифр - OpenSSL AES с предварительно открытым ключом.
Я использовал библиотеку not-yet-common-ssl. Однако он, похоже, застрял в версии 0.3.x и без релизов почти через 2 года, а не с трафиком списка рассылки или видимым развитием. Я должен сделать вывод, что это по существу мертво.
Основываясь на некоторых дополнительных вопросах stackoverflow, я нашел Spring Безопасность и Encryptor4j, оба из которых, похоже, предлагают некоторую разумно упакованную текстовую кодировку. Однако попытка получить Spring Защитные шифры для работы с декодированием известной кодированной текстовой строки не удалась для меня, я предполагаю, что генерация IV и Key, используемая OpenSSL, просто не поддерживается в поставляемой реализации.
Изучив приведенный выше код, а также известную рабочую реализацию С# и PHP, я смог придумать класс утилиты, который в настоящее время передает мои тесты для взаимодействия. Обычно я бы предпочел использовать известную библиотеку, но если есть, я не смог ее найти. Класс (https://gist.github.com/rrsIPOV/4d0f6be7c58173c16e9edf9f97c7d7f2) выглядит следующим образом:
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 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;
}
}
Ответ 3
Вы можете посмотреть эту дискуссию, указав алгоритм генерации ключей как конкатенацию двух хэшей MD5.
Относительно соли, упомянутой там, opensssl enc man page говорит:
Когда используется соль, первые восемь байтов зашифрованного данные зарезервированы для соли: она генерируется случайным образом, когда шифрование файла и чтение из зашифрованного файла, когда оно расшифрованы.
Ответ 4
В это время версия openssl 1.1.0f-3 требует дайджест-функции SHA-256. Без этого он не может декодироваться.