ECDH в SJCL реализуется через эллиптическую криптографию, встроенную
в модуль sjcl.ecc. Основная идея защищённого канала на базе
ECDH заключается в том, что две стороны обмениваются публичными ключами,
независимо вычисляют общий секрет и далее используют его как основу для
симметричного шифрования сообщений.
Обмен ключами по схеме Диффи–Хеллмана на эллиптических кривых
опирается на математическое свойство: при наличии приватного ключа
a и публичного ключа A = a * G, а также
приватного ключа b и публичного ключа
B = b * G, обе стороны могут вычислить один и тот же
секрет:
S = aB = bA
Этот секрет не передаётся по сети и не может быть восстановлен напрямую из публичных значений при корректных параметрах кривой.
В SJCL эллиптическая криптография реализована через
sjcl.ecc.elGamal и sjcl.ecc.curves.
Для начала каждой стороне требуется создать собственную пару ключей.
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 строится по следующей схеме:
Для повышения безопасности используется эпемерный ECDH (ECDHE), где ключи создаются для каждой сессии:
const sessionKeys = sjcl.ecc.elGamal.generateKeys(256);
После завершения сессии приватные ключи уничтожаются, что обеспечивает forward secrecy — невозможность расшифровать старые сообщения даже при компрометации ключа в будущем.
ECDH сам по себе не защищён от MITM-атаки. Если злоумышленник подменяет публичные ключи, он может установить два отдельных секрета с каждой стороной.
Для предотвращения этого используется аутентификация:
Пример проверки fingerprint:
const fingerprint = sjcl.hash.sha256.hash(publicKey.serialize());
Далее fingerprint сравнивается через защищённый канал или внешний доверенный источник.
Типичная структура сообщения в таком канале:
{
"iv": "base64...",
"data": "base64...",
"senderKeyId": "id",
"timestamp": 1710000000
}
Поле timestamp помогает защититься от replay-атак, если
оно дополнительно проверяется на стороне получателя.
Сессия устанавливается следующим образом:
sharedSecretsharedSecret выводится aesKey1. Использование сырого ECDH-секрета без KDF Это приводит к слабой и предсказуемой криптографии.
2. Повторное использование IV в AES-GCM Повтор IV полностью компрометирует безопасность канала.
3. Отсутствие аутентификации публичных ключей Открывает возможность MITM-атаки.
4. Хранение приватных ключей в памяти слишком долго Увеличивает поверхность атаки при утечке памяти.
Более надёжная схема включает дополнительный шаг:
const hmac = new sjcl.misc.hmac(aesKey, sjcl.hash.sha256);
const tag = hmac.encrypt(handshakeTranscript);
Это позволяет убедиться, что обе стороны используют одинаковый секрет.
Качество ECDH напрямую зависит от генерации ключей. Перед использованием обязательно инициализируется PRNG:
sjcl.random.startCollectors();
Без достаточной энтропии ключи могут стать предсказуемыми.
ECDH на SJCL часто применяется в:
При этом SJCL обычно выступает криптографическим слоем, а транспорт реализуется отдельно.
Хотя SJCL автономен, в современных браузерах часто используется гибридный подход:
Это позволяет повысить производительность при сохранении совместимости.
Общая последовательность обработки выглядит так:
Каждый этап критичен: ошибка в любом из них приводит к полной компрометации канала или потере целостности данных.