EvpKDF: совместимость с OpenSSL

В CryptoJS под совместимостью с OpenSSL чаще всего подразумевается реализация классического алгоритма выведения ключа EVP_BytesToKey. Это функция, которая исторически используется OpenSSL для преобразования пароля в криптографический ключ и вектор инициализации (IV) для симметричного шифрования.

Основная идея EVP_BytesToKey заключается в последовательном применении хэш-функции к паролю и соли до тех пор, пока не будет получено достаточно байт для ключа и IV.

OpenSSL по умолчанию использует следующую схему:

  • Хэш-функция: MD5 (в старых режимах)
  • Соль: 8 байт
  • Итерации: обычно 1 (но могут быть увеличены)
  • Выход: ключ + IV

Формат данных OpenSSL и роль префикса Salted__

При шифровании через OpenSSL CLI результат обычно кодируется в Base64 и содержит специальный заголовок:

Salted__ + 8 байт соли + зашифрованные данные

Этот префикс является критическим для совместимости, поскольку позволяет корректно восстановить ключ и IV на стороне CryptoJS.

Структура выглядит следующим образом:

  • первые 8 байт: ASCII строка Salted__
  • следующие 8 байт: соль
  • остальное: ciphertext

CryptoJS при расшифровке проверяет наличие этого заголовка и извлекает соль автоматически.


EVP_BytesToKey в CryptoJS

В CryptoJS используется совместимый алгоритм генерации ключа через CryptoJS.EvpKDF. Он реализует ту же идею, что и OpenSSL EVP_BytesToKey, но с расширенными возможностями выбора хэш-функции и количества итераций.

Ключевые параметры:

  • keySize — размер ключа в словах (32-битных)
  • ivSize — размер IV
  • iterations — число проходов хэширования
  • hasher — функция хэширования (MD5, SHA1, SHA256 и др.)
  • salt — случайная соль (8 байт)

Пример генерации ключа через EvpKDF

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 CLI

Шифрование через 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 автоматически:

  • декодирует Base64
  • извлекает Salted__
  • достаёт соль
  • применяет EVP_BytesToKey (MD5)
  • восстанавливает ключ и IV
  • выполняет AES-дешифрование

Отличия CryptoJS от OpenSSL EVP_BytesToKey

Несмотря на заявленную совместимость, существуют важные нюансы.

1. Хэш-функция

OpenSSL по умолчанию использует MD5:

key = MD5(password + salt)

CryptoJS позволяет заменить алгоритм:

CryptoJS.EvpKDF(password, salt, {
  hasher: CryptoJS.algo.SHA256
});

Это уже несовместимо с классическим OpenSSL режимом.


2. Количество итераций

OpenSSL CLI по умолчанию использует 1 итерацию. Однако современные версии поддерживают -iter.

CryptoJS также поддерживает:

iterations: 1000

Но если OpenSSL использует стандартную 1 итерацию, а CryptoJS — другую, результат будет несовместим.


3. Размер ключа и IV

OpenSSL для AES-256-CBC требует:

  • key: 32 байта
  • iv: 16 байт

CryptoJS требует ручного контроля через keySize.

Ошибка в расчётах приводит к несовместимости даже при одинаковом пароле.


Ручная совместимость с OpenSSL EVP_BytesToKey

Для полной имитации 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));

Шифрование с совместимым форматом OpenSSL

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);

Дешифрование OpenSSL-формата

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);
}

Типичные причины несовместимости

Использование SHA256 вместо MD5

OpenSSL старого формата не понимает SHA256-деривацию.

Неправильное извлечение соли

Соль всегда строго 8 байт после Salted__.

Ошибка в размере keySize

CryptoJS работает в 32-битных словах, а не байтах.

Отсутствие режима CBC

OpenSSL enc использует CBC по умолчанию.


Влияние современных стандартов OpenSSL

Начиная с OpenSSL 1.1.1 и выше, EVP_BytesToKey считается устаревшим, но продолжает поддерживаться для обратной совместимости.

Рекомендуется переход на:

  • PBKDF2
  • scrypt
  • HKDF

Однако CryptoJS в основном ориентирован на поддержку старых схем, поэтому EVP_KDF остаётся ключевым элементом совместимости.


Сравнение EVP_BytesToKey и PBKDF2 в CryptoJS

CryptoJS.PBKDF2(password, salt, {
  keySize: 256 / 32,
  iterations: 10000
});

PBKDF2:

  • медленнее
  • безопаснее
  • несовместим с OpenSSL enc по умолчанию

EVP_BytesToKey:

  • быстрый
  • устаревший
  • полностью совместим с OpenSSL CLI legacy режимом

Практическая модель совместимости

Для стабильной интеграции CryptoJS с OpenSSL необходимо придерживаться следующих условий:

  • AES-256-CBC
  • MD5 как hash-функция
  • salt = 8 bytes
  • iterations = 1
  • формат Salted__ + salt + ciphertext
  • Base64 encoding результата

Любое отклонение от этих параметров приводит к невозможности взаимной расшифровки данных между системами.