Реализация защищённого канала на основе ECDH

ECDH в SJCL реализуется через эллиптическую криптографию, встроенную в модуль sjcl.ecc. Основная идея защищённого канала на базе ECDH заключается в том, что две стороны обмениваются публичными ключами, независимо вычисляют общий секрет и далее используют его как основу для симметричного шифрования сообщений.

Обмен ключами по схеме Диффи–Хеллмана на эллиптических кривых опирается на математическое свойство: при наличии приватного ключа a и публичного ключа A = a * G, а также приватного ключа b и публичного ключа B = b * G, обе стороны могут вычислить один и тот же секрет:

S = aB = bA

Этот секрет не передаётся по сети и не может быть восстановлен напрямую из публичных значений при корректных параметрах кривой.

В SJCL эллиптическая криптография реализована через sjcl.ecc.elGamal и sjcl.ecc.curves.

Генерация ключевой пары в SJCL

Для начала каждой стороне требуется создать собственную пару ключей.

const keys = sjcl.ecc.elGamal.generateKeys(256);

const privateKey = keys.sec;   // приватный ключ
const publicKey  = keys.pub;   // публичный ключ

Здесь используется ElGamal поверх эллиптических кривых, где приватный ключ остаётся локально, а публичный передаётся второй стороне.

Важно, что SJCL использует безопасный генератор случайных чисел (sjcl.random), который должен быть предварительно инициализирован энтропией.

Обмен публичными ключами

После генерации каждая сторона передаёт публичный ключ другой стороне. Формат обмена обычно сериализуется:

const pubSerialized = sjcl.codec.base64.fromBits(publicKey.get());

На принимающей стороне:

const pubBits = sjcl.codec.base64.toBits(pubSerialized);
const remotePub = new sjcl.ecc.elGamal.publicKey(
    sjcl.ecc.curves.c256,
    sjcl.bn.fromBits(pubBits)
);

После восстановления публичного ключа можно переходить к вычислению общего секрета.

Вычисление общего секрета

SJCL предоставляет метод kem (Key Encapsulation Mechanism), который упрощает процесс получения общего секрета.

const sharedSecret = privateKey.deriveSecret(remotePub);

Результат — битовый массив (sjcl.bitArray), который нельзя использовать напрямую для шифрования. Его необходимо преобразовать в симметричный ключ.

Преобразование секрета в симметричный ключ

Сырой ECDH-результат не обладает нужной криптографической структурой, поэтому применяется KDF (Key Derivation Function). В SJCL часто используется SHA-256 или PBKDF2.

Простейший вариант:

const keyBits = sjcl.hash.sha256.hash(sharedSecret);
const aesKey = new sjcl.cipher.aes(keyBits);

Таким образом создаётся ключ для AES-256.

Построение защищённого канала

После того как обе стороны получили одинаковый aesKey, можно строить канал обмена сообщениями.

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

function encryptMessage(aesKey, plaintext) {
    const iv = sjcl.random.randomWords(4); // 128-bit IV
    const encrypted = sjcl.mode.gcm.encrypt(
        aesKey,
        sjcl.codec.utf8String.toBits(plaintext),
        iv
    );

    return {
        iv: sjcl.codec.base64.fromBits(iv),
        data: sjcl.codec.base64.fromBits(encrypted)
    };
}

Используется режим GCM, который обеспечивает одновременно конфиденциальность и целостность данных.

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

function decryptMessage(aesKey, payload) {
    const iv = sjcl.codec.base64.toBits(payload.iv);
    const data = sjcl.codec.base64.toBits(payload.data);

    const decrypted = sjcl.mode.gcm.decrypt(aesKey, data, iv);

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

Если сообщение было изменено, GCM выбросит исключение при проверке аутентичности.

Архитектура защищённого канала

На практике защищённый канал на ECDH в SJCL строится по следующей схеме:

  1. Генерация ключевой пары каждой стороной
  2. Обмен публичными ключами
  3. Вычисление общего секрета
  4. Производная ключа через SHA-256
  5. Инициализация AES-GCM
  6. Обмен зашифрованными сообщениями

Эфемерность ключей

Для повышения безопасности используется эпемерный ECDH (ECDHE), где ключи создаются для каждой сессии:

const sessionKeys = sjcl.ecc.elGamal.generateKeys(256);

После завершения сессии приватные ключи уничтожаются, что обеспечивает forward secrecy — невозможность расшифровать старые сообщения даже при компрометации ключа в будущем.

Защита от атак посредника (MITM)

ECDH сам по себе не защищён от MITM-атаки. Если злоумышленник подменяет публичные ключи, он может установить два отдельных секрета с каждой стороной.

Для предотвращения этого используется аутентификация:

  • проверка fingerprint публичных ключей
  • цифровые подписи
  • предварительный обмен доверенными ключами
  • использование TLS как транспортного уровня

Пример проверки fingerprint:

const fingerprint = sjcl.hash.sha256.hash(publicKey.serialize());

Далее fingerprint сравнивается через защищённый канал или внешний доверенный источник.

Формат защищённого сообщения

Типичная структура сообщения в таком канале:

{
  "iv": "base64...",
  "data": "base64...",
  "senderKeyId": "id",
  "timestamp": 1710000000
}

Поле timestamp помогает защититься от replay-атак, если оно дополнительно проверяется на стороне получателя.

Практическая схема сессии

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

  1. Клиент A создаёт ключи и отправляет публичный ключ B
  2. Клиент B отвечает своим публичным ключом
  3. Обе стороны вычисляют sharedSecret
  4. Из sharedSecret выводится aesKey
  5. Начинается зашифрованный обмен сообщениями

Типичные ошибки при использовании SJCL ECDH

1. Использование сырого ECDH-секрета без KDF Это приводит к слабой и предсказуемой криптографии.

2. Повторное использование IV в AES-GCM Повтор IV полностью компрометирует безопасность канала.

3. Отсутствие аутентификации публичных ключей Открывает возможность MITM-атаки.

4. Хранение приватных ключей в памяти слишком долго Увеличивает поверхность атаки при утечке памяти.

Усиленная модель канала с подтверждением ключей

Более надёжная схема включает дополнительный шаг:

  • после обмена ключами стороны подтверждают fingerprint
  • создаётся HMAC от handshake-данных
const hmac = new sjcl.misc.hmac(aesKey, sjcl.hash.sha256);
const tag = hmac.encrypt(handshakeTranscript);

Это позволяет убедиться, что обе стороны используют одинаковый секрет.

Работа с энтропией в SJCL

Качество ECDH напрямую зависит от генерации ключей. Перед использованием обязательно инициализируется PRNG:

sjcl.random.startCollectors();

Без достаточной энтропии ключи могут стать предсказуемыми.

Использование каналов в реальных приложениях

ECDH на SJCL часто применяется в:

  • защищённых WebSocket соединениях
  • end-to-end шифровании сообщений
  • P2P браузерных приложениях
  • защищённых API без TLS (внутренние протоколы)

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

Интеграция с WebCrypto как альтернативой

Хотя SJCL автономен, в современных браузерах часто используется гибридный подход:

  • ECDH через WebCrypto
  • симметричное шифрование через SJCL или наоборот

Это позволяет повысить производительность при сохранении совместимости.

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

Общая последовательность обработки выглядит так:

  1. генерация ключей
  2. обмен публичными данными
  3. вычисление ECDH-секрета
  4. применение SHA-256
  5. инициализация AES-GCM
  6. шифрование сообщений
  7. проверка целостности при расшифровании

Каждый этап критичен: ошибка в любом из них приводит к полной компрометации канала или потере целостности данных.