Работа с парольной защитой ключей (encrypted PEM)

PEM (Privacy-Enhanced Mail) — текстовый формат представления криптографических ключей и сертификатов. В контексте защиты приватных ключей используется механизм симметричного шифрования, при котором содержимое PEM-блока зашифровано с использованием пароля.

Зашифрованный PEM имеет характерную структуру:

-----BEGIN ENCRYPTED PRIVATE KEY-----
...
-----END ENCRYPTED PRIVATE KEY-----

или:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,ABCD1234...

...
-----END RSA PRIVATE KEY-----

Ключевые особенности:

  • Наличие заголовков Proc-Type и DEK-Info указывает на использование устаревшего формата PKCS#1 с симметричным шифрованием.
  • Формат ENCRYPTED PRIVATE KEY соответствует PKCS#8 и считается более современным.

Библиотека Jsrsasign поддерживает оба варианта.


Загрузка зашифрованного приватного ключа

Основной класс для работы с ключами — KEYUTIL. Метод getKey автоматически определяет тип ключа и при необходимости выполняет расшифровку.

Пример загрузки:

const encryptedPEM = `-----BEGIN ENCRYPTED PRIVATE KEY-----
...
-----END ENCRYPTED PRIVATE KEY-----`;

const password = "strongpassword";

const keyObj = KEYUTIL.getKey(encryptedPEM, password);

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

  • Второй аргумент обязателен для зашифрованных ключей.
  • При неверном пароле выбрасывается исключение.
  • Возвращается объект ключа (RSAKey, ECDSA, DSA и др.).

Поддерживаемые алгоритмы шифрования

Jsrsasign реализует поддержку популярных схем:

Для PKCS#1:

  • DES-CBC
  • DES-EDE3-CBC (Triple DES)
  • AES-128-CBC
  • AES-192-CBC
  • AES-256-CBC

Для PKCS#8:

  • PBES2 (Password-Based Encryption Scheme 2)
  • PBKDF2 (вывод ключа из пароля)
  • Различные комбинации HMAC и AES

Пример строки DEK-Info:

DEK-Info: AES-256-CBC,0123456789ABCDEF
  • Первая часть — алгоритм
  • Вторая — IV (инициализационный вектор)

Обработка ошибок при расшифровке

При работе с зашифрованными PEM важно учитывать возможные ошибки:

  1. Неверный пароль

    try {
        KEYUTIL.getKey(pem, "wrongpass");
    } catch (e) {
        console.error("Ошибка расшифровки:", e);
    }
  2. Повреждённый PEM

    • Нарушена структура Base64
    • Отсутствуют заголовки
  3. Неподдерживаемый алгоритм

    • Некоторые редкие схемы могут не поддерживаться

Проверка типа ключа после загрузки

После расшифровки полезно определить тип ключа:

if (keyObj instanceof RSAKey) {
    console.log("RSA ключ");
} else if (keyObj.type === "EC") {
    console.log("EC ключ");
}

Также доступно свойство:

console.log(keyObj.isPrivate); // true

Генерация зашифрованного PEM

Jsrsasign позволяет не только читать, но и создавать зашифрованные ключи.

Пример генерации RSA-ключа и его шифрования:

const kp = KEYUTIL.generateKeypair("RSA", 2048);

const encryptedPEM = KEYUTIL.getPEM(kp.prvKeyObj, "PKCS8PRV", "password123");

Параметры:

  • "PKCS8PRV" — формат
  • "password123" — пароль

Результат — строка PEM с шифрованием.


Настройка параметров шифрования

Можно явно задать алгоритмы:

const pem = KEYUTIL.getPEM(
    kp.prvKeyObj,
    "PKCS8PRV",
    "password123",
    {
        encalg: "aes256-cbc",
        iter: 10000
    }
);

Параметры:

  • encalg — алгоритм шифрования
  • iter — количество итераций PBKDF2

Чем выше iter, тем выше стойкость к перебору.


Преобразование между форматами

Частая задача — конвертация старого PKCS#1 PEM в современный PKCS#8:

const key = KEYUTIL.getKey(oldPem, password);

const newPem = KEYUTIL.getPEM(key, "PKCS8PRV", "newpassword");

Преимущества PKCS#8:

  • Стандартизированная структура
  • Поддержка современных алгоритмов
  • Лучшая совместимость

Извлечение незашифрованного ключа

Иногда требуется получить открытый (незашифрованный) вариант:

const key = KEYUTIL.getKey(encryptedPem, password);

const plainPem = KEYUTIL.getPEM(key, "PKCS1PRV");

Результат:

  • Ключ без защиты
  • Использовать с осторожностью

Работа с публичными ключами

Публичные ключи не шифруются паролем, но могут быть извлечены из приватного:

const key = KEYUTIL.getKey(encryptedPem, password);

const pubPem = KEYUTIL.getPEM(key, "PKCS8PUB");

Безопасность хранения паролей

Критические аспекты:

  • Пароль не должен храниться в исходном коде
  • Использование переменных окружения
  • Ограничение доступа к памяти

Пример:

const password = process.env.KEY_PASSWORD;

Производительность и ограничения

Расшифровка включает:

  • PBKDF2 (дорогая операция)
  • Симметричное дешифрование

Факторы влияния:

  • Длина ключа
  • Количество итераций
  • Алгоритм (AES быстрее DES)

Практика: подпись с использованием зашифрованного ключа

const sig = new KJUR.crypto.Signature({ alg: "SHA256withRSA" });

const key = KEYUTIL.getKey(encryptedPem, password);

sig.init(key);
sig.updateString("data");

const signature = sig.sign();

Ключ загружается один раз, затем используется в криптооперациях.


Практика: проверка подписи

const sig = new KJUR.crypto.Signature({ alg: "SHA256withRSA" });

sig.init(publicKey);
sig.updateString("data");

const isValid = sig.verify(signature);

Частые проблемы

1. “malformed PEM”

  • Лишние пробелы
  • Неправильные переносы строк

2. “unsupported algorithm”

  • Старые схемы шифрования

3. “password mismatch”

  • Неверная кодировка строки (UTF-8 vs ASCII)

Внутренний механизм расшифровки

Процесс включает:

  1. Парсинг PEM
  2. Определение формата (PKCS#1 или PKCS#8)
  3. Извлечение параметров (salt, IV)
  4. Вывод ключа через PBKDF2
  5. Расшифровка симметричным алгоритмом
  6. ASN.1-декодирование

Jsrsasign реализует это полностью на JavaScript без нативных зависимостей.


Рекомендации по использованию

  • Предпочтение PKCS#8
  • Использование AES-256
  • Минимум 10 000 итераций PBKDF2
  • Изоляция паролей от кода
  • Очистка чувствительных данных из памяти при возможности

Проверка корректности PEM

Перед загрузкой полезно выполнить базовую проверку:

if (!pem.includes("BEGIN")) {
    throw new Error("Некорректный PEM");
}

Итерации PBKDF2 и стойкость

PBKDF2 замедляет перебор паролей:

  • 1 000 итераций — устарело
  • 10 000 — минимально допустимо
  • 100 000+ — рекомендуется

Работа в браузере и Node.js

Jsrsasign полностью кроссплатформенен:

  • Не требует Node.js crypto API
  • Работает в браузере
  • Подходит для клиентских приложений

Однако:

  • В браузере сложнее защитить пароль
  • Возможны утечки через DevTools

Обработка больших ключей

Для ключей 4096+ бит:

  • Увеличивается время расшифровки
  • Возрастает нагрузка на CPU

Оптимизация:

  • Кэширование объекта ключа
  • Избегание повторной расшифровки

Кэширование ключа

let cachedKey = null;

function getKey() {
    if (!cachedKey) {
        cachedKey = KEYUTIL.getKey(pem, password);
    }
    return cachedKey;
}

Интеграция с другими библиотеками

Jsrsasign может использоваться совместно с:

  • WebCrypto API
  • OpenSSL (через совместимые PEM)
  • JWT-библиотеками

Пример использования в JWT:

const header = { alg: "RS256", typ: "JWT" };
const payload = { sub: "123456" };

const sJWT = KJUR.jws.JWS.sign(
    "RS256",
    JSON.stringify(header),
    JSON.stringify(payload),
    KEYUTIL.getKey(encryptedPem, password)
);

Особенности кодировок

Пароль интерпретируется как UTF-8 строка. Ошибки возможны при:

  • Использовании нестандартных символов
  • Несовпадении кодировок между системами

Работа с DER

Если ключ в бинарном формате:

const key = KEYUTIL.getKeyFromEncryptedPKCS8PEM(pem, password);

Для DER требуется предварительное преобразование в PEM.


Ограничения библиотеки

  • Нет аппаратного ускорения
  • Ограниченная поддержка экзотических алгоритмов
  • Вся криптография выполняется в JS (медленнее нативных решений)

Структура PKCS#8 (упрощённо)

ASN.1 структура включает:

  • AlgorithmIdentifier
  • EncryptedData
  • Parameters (salt, iterations)

Jsrsasign разбирает ASN.1 через встроенные модули.


Вывод ключевых принципов

  • Зашифрованный PEM — стандарт защиты приватных ключей
  • Jsrsasign обеспечивает полный цикл работы: чтение, расшифровка, генерация
  • Безопасность зависит не только от алгоритма, но и от управления паролем
  • Предпочтение современным форматам и высоким параметрам PBKDF2