Шифрование для нескольких получателей

Шифрование для нескольких получателей — это задача, при которой один отправитель должен передать зашифрованные данные сразу нескольким сторонам так, чтобы:

  • каждое из получающих лиц могло расшифровать сообщение;
  • посторонние не имели доступа к содержимому;
  • не требовалось шифровать исходные данные отдельно для каждого получателя (из соображений эффективности).

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


Базовый подход: гибридное шифрование

Классическая схема шифрования для нескольких получателей строится на гибридном подходе:

  1. Генерируется случайный симметричный ключ (session key).

  2. Сообщение шифруется этим ключом (например, с помощью AES).

  3. Для каждого получателя:

    • симметричный ключ шифруется его публичным ключом.
  4. Отправляется:

    • зашифрованное сообщение;
    • набор зашифрованных симметричных ключей (по одному на каждого получателя).

Такой подход позволяет:

  • избежать повторного шифрования данных;
  • масштабировать количество получателей;
  • использовать преимущества симметричного и асимметричного шифрования.

Генерация симметричного ключа

В SJCL используется криптографически стойкий генератор случайных чисел:

const sessionKey = sjcl.random.randomWords(8, 0); // 256 бит

Ключ представляется в виде массива слов (bitArray), что является стандартным форматом SJCL.


Шифрование сообщения

Для шифрования данных применяется AES:

const plaintext = "Секретное сообщение";

const encryptedData = sjcl.encrypt(sessionKey, plaintext);

Важно:

  • sessionKey здесь используется как пароль (SJCL автоматически обрабатывает его);
  • результат — JSON-строка с параметрами шифрования (IV, salt, ciphertext и т.д.).

Ассиметричное шифрование ключа

SJCL поддерживает эллиптическую криптографию (ECC), включая ECDH и ElGamal-подобные схемы.

Генерация ключей получателей

const keys = sjcl.ecc.elGamal.generateKeys(256);
const publicKey = keys.pub;
const privateKey = keys.sec;

Шифрование симметричного ключа

const encryptedKey = publicKey.kem();

Метод kem() (Key Encapsulation Mechanism):

  • генерирует общий секрет;

  • возвращает:

    • key: симметричный ключ;
    • tag: данные для передачи получателю.

Однако здесь есть важный момент: нужно использовать полученный key для шифрования sessionKey.


Полная схема для нескольких получателей

Шаг 1: Генерация session key

const sessionKey = sjcl.random.randomWords(8, 0);

Шаг 2: Шифрование данных

const encryptedMessage = sjcl.encrypt(sessionKey, "Секретное сообщение");

Шаг 3: Обработка каждого получателя

const recipients = [pubKey1, pubKey2, pubKey3];

const encryptedKeys = recipients.map(pubKey => {
    const kem = pubKey.kem();

    const wrappedKey = sjcl.encrypt(kem.key, sessionKey);

    return {
        tag: kem.tag,
        wrappedKey: wrappedKey
    };
});

В результате:

  • kem.key — общий секрет (уникальный для каждого получателя);
  • wrappedKey — sessionKey, зашифрованный этим секретом.

Формирование итогового пакета

const payload = {
    data: encryptedMessage,
    keys: encryptedKeys
};

Структура:

  • data — зашифрованное сообщение;

  • keys — массив объектов для каждого получателя:

    • tag — данные для восстановления ключа;
    • wrappedKey — зашифрованный session key.

Расшифровка на стороне получателя

Шаг 1: Найти свой ключ

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

const kem = privateKey.unkem(tag);

Если ключ подходит:

  • kem даст тот же симметричный ключ, который использовался для шифрования sessionKey.

Шаг 2: Расшифровать session key

const sessionKey = sjcl.decrypt(kem, wrappedKey);

Шаг 3: Расшифровать сообщение

const plaintext = sjcl.decrypt(sessionKey, payload.data);

Важные аспекты безопасности

1. Уникальность ключей

Каждый вызов kem() генерирует новый ключ. Это гарантирует:

  • криптографическую независимость между получателями;
  • невозможность компрометации всей системы при утечке одного ключа.

2. Защита от повторного использования

Session key должен быть:

  • случайным;
  • одноразовым.

Повторное использование ключа снижает безопасность.


3. Аутентификация

SJCL по умолчанию использует режимы с аутентификацией (например, CCM), но важно учитывать:

  • защита от подмены данных;
  • проверка целостности.

4. Проверка получателя

При реализации необходимо учитывать:

  • механизм идентификации получателя;
  • соответствие ключей.

Иначе возможны атаки типа:

  • подмена ключа;
  • попытка дешифрования чужих данных.

Оптимизация и масштабирование

Уменьшение размера данных

При большом числе получателей:

  • массив keys растёт линейно;

  • каждый элемент содержит:

    • tag;
    • wrappedKey.

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

  • использовать компактные форматы сериализации;
  • применять сжатие (например, перед передачей).

Параллельная обработка

Шифрование для каждого получателя может выполняться параллельно:

Promise.all(recipients.map(pubKey => {
    return new Promise(resolve => {
        const kem = pubKey.kem();
        const wrappedKey = sjcl.encrypt(kem.key, sessionKey);

        resolve({
            tag: kem.tag,
            wrappedKey
        });
    });
}));

Альтернативные подходы

1. Использование одного общего ключа

Недостатки:

  • при компрометации одного получателя уязвимы все;
  • отсутствует изоляция.

2. Повторное шифрование данных

message → encrypt(pubKey1)
message → encrypt(pubKey2)
...

Недостатки:

  • высокая вычислительная нагрузка;
  • увеличение объёма данных.

3. Broadcast Encryption

Более сложные схемы:

  • требуют специализированных библиотек;
  • не реализованы в SJCL напрямую.

Практические рекомендации

  • использовать ECC вместо RSA для лучшей производительности в браузере;
  • хранить публичные ключи в проверенных источниках;
  • применять дополнительные механизмы подписи (например, ECDSA);
  • избегать хранения session key в открытом виде даже временно;
  • очищать чувствительные данные из памяти.

Расширение схемы: подпись отправителя

Для подтверждения подлинности сообщения:

  1. Отправитель подписывает payload:
const signature = senderPrivateKey.sign(hash(payload));
  1. Получатель проверяет:
senderPublicKey.verify(hash(payload), signature);

Это защищает от:

  • подмены отправителя;
  • атак «man-in-the-middle».

Типичные ошибки

  • использование слабых случайных чисел;
  • повторное использование IV или ключей;
  • хранение приватных ключей в небезопасных местах;
  • отсутствие проверки целостности;
  • попытка «упростить» схему, убрав асимметричное шифрование.

Архитектурный итог

Схема шифрования для нескольких получателей в SJCL строится из следующих компонентов:

  • генерация случайного симметричного ключа;
  • шифрование данных этим ключом;
  • независимое шифрование ключа для каждого получателя через ECC;
  • передача набора зашифрованных ключей;
  • локальная расшифровка каждым получателем.

Такой подход сочетает:

  • эффективность симметричного шифрования;
  • гибкость асимметричных методов;
  • масштабируемость для произвольного числа участников.