Совместимость с OpenSSL

Совместимость между реализациями криптографии в JavaScript и OpenSSL осложняется тем, что они используют разные уровни абстракции и разные форматы представления зашифрованных данных.

OpenSSL ориентирован на потоковый формат командной строки и бинарные структуры с минимальной метаинформацией. SJCL (Stanford Javascript Crypto Library) напротив оперирует высокоуровневым JSON-форматом, в котором хранится вся информация о параметрах шифрования.

Ключевая проблема совместимости заключается не в самом AES, а в:

  • различиях KDF (key derivation function)
  • различиях формата контейнера
  • способе хранения соли и IV
  • кодировке результата

Формат данных OpenSSL (openssl enc)

При использовании openssl enc данные обычно имеют следующий вид:

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

Пример команды:

openssl enc -aes-256-cbc -salt -pbkdf2 -in file.txt -out file.enc -a

Особенности:

  • префикс Salted__ (8 байт)

  • соль (8 байт)

  • дальше ciphertext

  • часто применяется base64 (-a)

  • KDF зависит от параметров:

    • старый режим: EVP_BytesToKey
    • новый режим: PBKDF2 (если указан -pbkdf2)

Формат SJCL

SJCL использует JSON-структуру:

{
  "iv": "...",
  "v": 1,
  "iter": 1000,
  "ks": 256,
  "ts": 64,
  "mode": "ccm",
  "adata": "",
  "cipher": "aes",
  "salt": "...",
  "ct": "..."
}

Особенности:

  • всё кодируется в base64 внутри JSON
  • явно указаны параметры KDF
  • поддерживаются разные режимы (CBC, CCM, GCM через расширения)
  • встроенное использование PBKDF2

Ключевое различие KDF: EVP_BytesToKey vs PBKDF2

OpenSSL (по умолчанию)

key = EVP_BytesToKey(password, salt, md5, iterations=1)

Особенности:

  • быстрый, но устаревший
  • зависит от MD5
  • недостаточно устойчив к атаке перебора

OpenSSL с PBKDF2

-pbkdf2 -iter 10000

Используется:

PBKDF2-HMAC-SHA256(password, salt, iter)

SJCL

SJCL использует:

PBKDF2-HMAC-SHA256 (по умолчанию)

с параметрами:

  • iter: обычно 1000–10000
  • key size: ks / 32 байта

Соль и IV

OpenSSL

  • salt: 8 байт
  • IV: выводится из KDF (EVP_BytesToKey) или хранится отдельно логически

SJCL

  • salt: произвольная длина (обычно 8–16 байт)
  • IV: явно хранится в JSON (iv поле)

Режимы шифрования

OpenSSL

Поддержка через -aes-256-cbc, -aes-128-cbc и т.д.

  • CBC — основной режим
  • padding: PKCS#7

SJCL

Поддерживает:

  • AES-CCM (по умолчанию)
  • AES-CBC
  • AES-GCM (через расширения)

Критический момент:

  • OpenSSL чаще CBC
  • SJCL по умолчанию CCM → несовместимость без явного указания режима

Практическая совместимость OpenSSL → SJCL

Условие совместимости

Чтобы SJCL мог расшифровать данные OpenSSL:

  • должен использоваться AES-CBC
  • KDF должен быть PBKDF2
  • одинаковая соль
  • одинаковый IV (или его вывод)
  • одинаковый base64 формат ciphertext

Пример шифрования в OpenSSL

openssl enc -aes-256-cbc -salt -pbkdf2 -iter 10000 -in msg.txt -out msg.enc -a

Расшифровка в SJCL

OpenSSL формат нужно вручную разобрать:

function opensslDecrypt(base64, password) {
  const raw = sjcl.codec.base64.toBits(base64);

  const bytes = sjcl.codec.bytes.fromBits(raw);

  const saltHeader = bytes.slice(0, 8);
  const salt = bytes.slice(8, 16);
  const ciphertext = bytes.slice(16);

  const keyAndIv = sjcl.misc.pbkdf2(password, sjcl.codec.bytes.toBits(salt), 10000, 256 + 128);

  const key = keyAndIv.slice(0, 8);
  const iv = keyAndIv.slice(8, 12);

  const decrypted = sjcl.mode.cbc.decrypt(
    new sjcl.cipher.aes(key),
    sjcl.codec.bytes.toBits(ciphertext),
    iv
  );

  return sjcl.codec.utf8String.fromBits(decrypted);
}

Практическая совместимость SJCL → OpenSSL

SJCL шифрует в JSON, поэтому OpenSSL не может напрямую прочитать результат.

Пример SJCL:

const encrypted = sjcl.encrypt("password", "secret text", {
  iter: 10000,
  ks: 256,
  mode: "cbc"
});

Результат:

{"iv":"...","salt":"...","ct":"..."}

Подготовка данных для OpenSSL

Извлекаются:

  • salt
  • iv
  • ciphertext

Далее нужно собрать формат OpenSSL:

Salted__ + salt + ciphertext

и затем:

openssl enc -aes-256-cbc -d -salt -pbkdf2 -in file.enc

Base64 и бинарное представление

OpenSSL

  • base64 применяется как внешний слой (-a)
  • внутри данных бинарный поток

SJCL

  • base64 используется для всех бинарных частей внутри JSON
  • каждое поле кодируется отдельно

Это создаёт несоответствие:

  • OpenSSL: один base64 поток
  • SJCL: структурированные base64 поля

Кодировка текста и UTF-8

Критический момент при совместимости:

  • OpenSSL работает с байтовыми строками
  • SJCL работает с битовыми массивами (bitArray)

Ошибки возникают при:

  • кириллице
  • emoji
  • многобайтовых UTF-8 символах

Рекомендуемая модель:

  • всегда явно использовать UTF-8 перед шифрованием
  • после дешифрования выполнять utf8String.fromBits

PKCS#7 padding

Оба решения используют PKCS#7 в CBC режиме:

  • OpenSSL делает padding автоматически
  • SJCL также применяет padding при CBC

Несовместимость возникает только при:

  • неправильном извлечении IV
  • неправильной длине ключа

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

1. Разный KDF

  • EVP_BytesToKey ≠ PBKDF2

2. Разный режим AES

  • CBC vs CCM

3. Отсутствие соли в нужном формате

4. Ошибка извлечения IV

5. Неверная base64 декодировка

6. Различие в iter count PBKDF2


Практические рекомендации для унификации

Использовать только PBKDF2

OpenSSL:

-pbkdf2 -iter 10000

SJCL:

iter: 10000

Использовать AES-CBC

  • SJCL: mode: "cbc"
  • OpenSSL: -aes-256-cbc

Явно фиксировать параметры

  • key size: 256 бит
  • salt size: 8 байт
  • IV size: 16 байт

Не использовать EVP_BytesToKey

Он создаёт фундаментальную несовместимость с SJCL.


Итоговая модель совместимости

Наиболее стабильная схема взаимодействия:

  • AES-256-CBC
  • PBKDF2-HMAC-SHA256
  • iter ≥ 10000
  • salt 8–16 bytes
  • IV 16 bytes
  • UTF-8 encoding
  • base64 как внешний слой

При соблюдении этих условий SJCL и OpenSSL могут работать в рамках одной криптографической схемы без потери совместимости.