Совместимость шифрования с серверными языками

Базовые принципы межъязыковой криптографии

Криптографические алгоритмы сами по себе стандартизированы, но их реализации в разных экосистемах часто отличаются деталями, которые критически влияют на совместимость. При использовании Crypto-js на стороне JavaScript и серверных библиотек (PHP, Java, Python, Go) основная проблема заключается не в алгоритмах, а в представлении данных, режимах работы и параметрах ключей.

Ключевые факторы, определяющие совместимость:

  • формат входных и выходных данных (Base64, Hex, UTF-8, WordArray)
  • режим шифрования (CBC, ECB, GCM)
  • способ генерации IV
  • алгоритм получения ключа из пароля
  • padding (PKCS7, PKCS5, NoPadding)
  • формат упаковки результата (особенно OpenSSL-совместимый)

Представление данных и кодировки

Crypto-js оперирует внутренним типом WordArray, который не совпадает напрямую с бинарными типами серверных языков. При передаче данных между системами чаще всего используются:

  • Base64 — наиболее универсальный формат
  • Hex — удобен для отладки, но увеличивает размер
  • UTF-8 — используется для исходного текста до шифрования

Типичная ошибка совместимости возникает при смешивании кодировок:

  • Jav * aScript: CryptoJS.enc.Utf8
  • PHP: mb_convert_encoding, openssl_encrypt
  • Python: bytes() / .encode('utf-8')

Даже при одинаковом алгоритме различие в кодировке приводит к полностью несовместимому шифртексту.


AES в Crypto-js и серверные реализации

Наиболее часто используется AES-128/192/256. В Crypto-js базовый пример выглядит следующим образом:

const CryptoJS = require("crypto-js");

const key = CryptoJS.enc.Utf8.parse("1234567890123456");
const iv = CryptoJS.enc.Utf8.parse("1234567890123456");

const encrypted = CryptoJS.AES.encrypt("message", key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
});

const result = encrypted.toString();

На серверной стороне эквивалент должен строго повторять:

  • режим CBC
  • PKCS7 padding
  • одинаковый IV
  • одинаковую длину ключа

Пример PHP:

$data = "message";
$key = "1234567890123456";
$iv = "1234567890123456";

$encrypted = openssl_encrypt(
    $data,
    "AES-128-CBC",
    $key,
    OPENSSL_RAW_DATA,
    $iv
);

echo base64_encode($encrypted);

Ключевой момент — Crypto-js при toString() возвращает Base64, тогда как PHP по умолчанию может возвращать бинарные данные.


OpenSSL-совместимый формат Crypto-js

При использовании пароля вместо ключа Crypto-js часто генерирует OpenSSL-совместимый формат:

Salted__ + salt + ciphertext

Это поведение возникает при:

CryptoJS.AES.encrypt("message", "password").toString();

В этом случае:

  • используется EVP_BytesToKey (OpenSSL legacy KDF)
  • автоматически добавляется salt
  • результат кодируется в Base64

На серверной стороне это вызывает несовместимость, если используется:

  • AES-256-CBC с ручным ключом
  • PBKDF2 вместо EVP_BytesToKey

PHP может расшифровать такой формат только при ручной реализации EVP-подобного KDF или использовании совместимых библиотек.


Генерация ключа: PBKDF2 и EVP_BytesToKey

Crypto-js поддерживает PBKDF2:

const key = CryptoJS.PBKDF2("password", salt, {
    keySize: 256 / 32,
    iterations: 1000
});

Серверные языки:

  • PHP: hash_pbkdf2
  • Java: SecretKeyFactory PBKDF2WithHmacSHA256
  • Python: hashlib.pbkdf2_hmac

Несовместимость возникает, когда:

  • Crypto-js использует PBKDF2
  • сервер использует EVP_BytesToKey (или наоборот)

EVP_BytesToKey не имеет параметра итераций в классическом виде, что делает результаты несовместимыми при одинаковом пароле.


IV (инициализационный вектор)

IV должен:

  • иметь фиксированную длину (для AES — 16 байт)
  • быть идентичным на обеих сторонах
  • не повторяться при разных сообщениях (в реальных системах)

Ошибка типична при:

  • строковом IV без приведения к байтам
  • различиях UTF-8 vs ASCII

Crypto-js:

CryptoJS.enc.Utf8.parse("1234567890123456")

PHP:

$iv = "1234567890123456";

Java:

new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));

Режимы шифрования и различия реализаций

Наиболее проблемные режимы:

  • ECB (не использует IV, но реализуется по-разному)
  • CBC (наиболее совместимый)
  • GCM (часто несовместим из-за auth tag)

Crypto-js:

  • CBC — полностью совместим при правильных параметрах
  • GCM — ограниченная поддержка и частые расхождения с OpenSSL/Java

Особенность GCM:

  • требуется authentication tag
  • разные библиотеки по-разному упаковывают результат

Padding и критические расхождения

Crypto-js использует:

  • Pkcs7 padding по умолчанию

Серверные аналоги:

  • PKCS5Padding (Java)
  • PKCS7Padding (PHP/OpenSSL)
  • NoPadding (требует выравнивания вручную)

Несовпадение padding приводит к ошибкам:

  • “bad decrypt”
  • “invalid padding”
  • “wrong final block length”

HMAC и хеш-функции

Для HMAC и SHA Crypto-js обычно совместим без проблем, так как отсутствует сложная бинарная упаковка.

Пример:

CryptoJS.HmacSHA256("message", "key").toString()

PHP:

hash_hmac("sha256", "message", "key");

Java:

Mac.getInstance("HmacSHA256");

Python:

hmac.new(key, msg, hashlib.sha256).hexdigest()

Критический момент — формат вывода:

  • hex (по умолчанию в PHP)
  • Base64 (часто в JS)
  • raw bytes (Java)

Типовые серверные экосистемы и особенности совместимости

PHP (OpenSSL)

  • тесная совместимость с OpenSSL-форматами
  • поддержка salted формата
  • удобен для CBC и AES

Проблема: различие в дефолтных опциях openssl_encrypt


Java (javax.crypto)

  • строгая типизация

  • явное указание трансформации:

    • "AES/CBC/PKCS5Padding"
  • требует ручного Base64 декодирования

Проблема: отсутствие OpenSSL salted формата


Python (cryptography / PyCrypto / hashlib)

  • гибкая работа с байтами
  • требует явного контроля padding и IV
  • легко воспроизводит Crypto-js при правильной настройке

Node.js (crypto)

  • наиболее близок к Crypto-js по модели данных
  • поддерживает OpenSSL EVP и PBKDF2
  • часто используется как мост между фронтендом и сервером

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

  • различие Base64 и Hex на выходе
  • отсутствие явного IV
  • использование пароля вместо ключа без согласования KDF
  • разные padding-режимы
  • OpenSSL salted формат без поддержки на сервере
  • неправильная интерпретация бинарных данных как строки

Практическая стратегия согласования реализаций

Для стабильной совместимости между Crypto-js и серверными языками обычно фиксируются:

  • AES-256-CBC как основной алгоритм
  • PBKDF2 с указанным числом итераций
  • Base64 как стандарт передачи
  • явный IV 16 байт
  • PKCS7 padding
  • отказ от автоматического OpenSSL salted формата

Такой набор параметров позволяет добиться детерминированного результата между JavaScript и серверными платформами без зависимости от внутренней реализации конкретной библиотеки