В CryptoJS под совместимостью с OpenSSL чаще всего подразумевается реализация классического алгоритма выведения ключа EVP_BytesToKey. Это функция, которая исторически используется OpenSSL для преобразования пароля в криптографический ключ и вектор инициализации (IV) для симметричного шифрования.
Основная идея EVP_BytesToKey заключается в последовательном применении хэш-функции к паролю и соли до тех пор, пока не будет получено достаточно байт для ключа и IV.
OpenSSL по умолчанию использует следующую схему:
При шифровании через OpenSSL CLI результат обычно кодируется в Base64 и содержит специальный заголовок:
Salted__ + 8 байт соли + зашифрованные данные
Этот префикс является критическим для совместимости, поскольку позволяет корректно восстановить ключ и IV на стороне CryptoJS.
Структура выглядит следующим образом:
Salted__CryptoJS при расшифровке проверяет наличие этого заголовка и извлекает соль автоматически.
В CryptoJS используется совместимый алгоритм генерации ключа через
CryptoJS.EvpKDF. Он реализует ту же идею, что и OpenSSL
EVP_BytesToKey, но с расширенными возможностями выбора хэш-функции и
количества итераций.
Ключевые параметры:
keySize — размер ключа в словах (32-битных)ivSize — размер IViterations — число проходов хэшированияhasher — функция хэширования (MD5, SHA1, SHA256 и
др.)salt — случайная соль (8 байт)const CryptoJS = require("crypto-js");
const password = "secret_password";
const salt = CryptoJS.lib.WordArray.random(8);
const keyIv = CryptoJS.EvpKDF(password, salt, {
keySize: 48 / 4, // 32 bytes key + 16 bytes IV = 48 bytes total
iterations: 1
});
const key = CryptoJS.lib.WordArray.create(keyIv.words.slice(0, 8));
const iv = CryptoJS.lib.WordArray.create(keyIv.words.slice(8, 12));
Здесь важно понимать, что CryptoJS возвращает единый буфер, который вручную разделяется на ключ и IV.
Шифрование через OpenSSL:
openssl enc -aes-256-cbc -salt -in file.txt -out file.enc -k "secret_password"
Результат можно расшифровать в CryptoJS следующим образом:
const CryptoJS = require("crypto-js");
const encryptedBase64 = "..."; // результат OpenSSL (base64)
const password = "secret_password";
const decrypted = CryptoJS.AES.decrypt(encryptedBase64, password);
const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
CryptoJS автоматически:
Salted__Несмотря на заявленную совместимость, существуют важные нюансы.
OpenSSL по умолчанию использует MD5:
key = MD5(password + salt)
CryptoJS позволяет заменить алгоритм:
CryptoJS.EvpKDF(password, salt, {
hasher: CryptoJS.algo.SHA256
});
Это уже несовместимо с классическим OpenSSL режимом.
OpenSSL CLI по умолчанию использует 1 итерацию. Однако современные
версии поддерживают -iter.
CryptoJS также поддерживает:
iterations: 1000
Но если OpenSSL использует стандартную 1 итерацию, а CryptoJS — другую, результат будет несовместим.
OpenSSL для AES-256-CBC требует:
CryptoJS требует ручного контроля через keySize.
Ошибка в расчётах приводит к несовместимости даже при одинаковом пароле.
Для полной имитации OpenSSL необходимо строго соблюдать параметры:
const CryptoJS = require("crypto-js");
function opensslKDF(password, salt) {
return CryptoJS.EvpKDF(password, salt, {
keySize: 48 / 4,
iterations: 1,
hasher: CryptoJS.algo.MD5
});
}
Далее разделение:
const derived = opensslKDF("secret", salt);
const key = CryptoJS.lib.WordArray.create(derived.words.slice(0, 8));
const iv = CryptoJS.lib.WordArray.create(derived.words.slice(8, 12));
CryptoJS может генерировать совместимый OpenSSL-формат вручную:
const CryptoJS = require("crypto-js");
const password = "secret_password";
const plaintext = "Hello world";
const salt = CryptoJS.lib.WordArray.random(8);
const keyIv = CryptoJS.EvpKDF(password, salt, {
keySize: 48 / 4,
iterations: 1
});
const key = CryptoJS.lib.WordArray.create(keyIv.words.slice(0, 8));
const iv = CryptoJS.lib.WordArray.create(keyIv.words.slice(8, 12));
const encrypted = CryptoJS.AES.encrypt(plaintext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const openSSLBinary =
CryptoJS.enc.Utf8.parse("Salted__").concat(salt).concat(encrypted.ciphertext);
const base64 = CryptoJS.enc.Base64.stringify(openSSLBinary);
const CryptoJS = require("crypto-js");
function decryptOpenSSL(base64, password) {
const data = CryptoJS.enc.Base64.parse(base64);
const salted = CryptoJS.enc.Utf8.stringify(data.slice(0, 8));
if (salted !== "Salted__") {
throw new Error("Invalid OpenSSL format");
}
const salt = CryptoJS.lib.WordArray.create(data.words.slice(2, 4));
const keyIv = CryptoJS.EvpKDF(password, salt, {
keySize: 48 / 4,
iterations: 1,
hasher: CryptoJS.algo.MD5
});
const key = CryptoJS.lib.WordArray.create(keyIv.words.slice(0, 8));
const iv = CryptoJS.lib.WordArray.create(keyIv.words.slice(8, 12));
const encrypted = CryptoJS.lib.CipherParams.create({
ciphertext: CryptoJS.lib.WordArray.create(data.words.slice(4))
});
const decrypted = CryptoJS.AES.decrypt(encrypted, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
OpenSSL старого формата не понимает SHA256-деривацию.
Соль всегда строго 8 байт после Salted__.
CryptoJS работает в 32-битных словах, а не байтах.
OpenSSL enc использует CBC по умолчанию.
Начиная с OpenSSL 1.1.1 и выше, EVP_BytesToKey считается устаревшим, но продолжает поддерживаться для обратной совместимости.
Рекомендуется переход на:
Однако CryptoJS в основном ориентирован на поддержку старых схем, поэтому EVP_KDF остаётся ключевым элементом совместимости.
CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 10000
});
PBKDF2:
EVP_BytesToKey:
Для стабильной интеграции CryptoJS с OpenSSL необходимо придерживаться следующих условий:
Salted__ + salt + ciphertextЛюбое отклонение от этих параметров приводит к невозможности взаимной расшифровки данных между системами.