В клиент-серверных системах симметричное шифрование требует предварительного согласования общего секрета, который не должен передаваться по сети в явном виде. В SJCL (Stanford Javascript Crypto Library) для этого используются схемы согласования ключей на основе асимметричной криптографии, чаще всего Elliptic Curve Diffie–Hellman (ECDH).
Суть модели заключается в том, что каждая сторона генерирует собственную пару ключей:
После обмена публичными ключами обе стороны независимо вычисляют одинаковое значение общего секрета.
В SJCL реализация ECC находится в пространстве sjcl.ecc.
Для схемы Diffie–Hellman на эллиптических кривых используется механизм,
основанный на умножении точки генератора на случайное число (приватный
ключ).
Ключевые компоненты:
sjcl.ecc.elGamal.generateKeys() — генерация пары
ключейsec.get() — получение приватного ключаpub — публичный ключmultiply() — операция ECDH для получения общего
секретаПример генерации ключей:
const keys = sjcl.ecc.elGamal.generateKeys();
const privateKey = keys.sec;
const publicKey = keys.pub;
Обмен ключами проходит в несколько этапов:
Клиент и сервер независимо создают свои ECC ключи:
// Клиент
const clientKeys = sjcl.ecc.elGamal.generateKeys();
// Сервер
const serverKeys = sjcl.ecc.elGamal.generateKeys();
Публичные ключи сериализуются и передаются по сети:
const clientPub = clientKeys.pub;
const serverPub = serverKeys.pub;
Каждая сторона использует свой приватный ключ и публичный ключ другой стороны:
// Клиент вычисляет общий секрет
const clientSecret = clientKeys.sec.derive(serverPub);
// Сервер вычисляет общий секрет
const serverSecret = serverKeys.sec.derive(clientPub);
В результате clientSecret и serverSecret
совпадают, хотя приватные ключи никогда не покидали устройства.
Полученный общий секрет нельзя напрямую использовать как AES-ключ. Он
представляет собой структуру bitArray, которую необходимо
привести к фиксированной длине через KDF (Key Derivation Function).
В SJCL часто используется:
sjcl.hash.sha256 — хешированиеsjcl.misc.pbkdf2 — если требуется усиление пароляПример преобразования:
const sharedBits = clientKeys.sec.derive(serverPub);
const sessionKey = sjcl.hash.sha256.hash(sharedBits);
Далее sessionKey может использоваться в симметричных
алгоритмах:
const aesKey = new sjcl.cipher.aes(sessionKey);
Ключевой проблемой при межсистемной совместимости является согласование:
bitArrayВ SJCL ECC ключи обычно сериализуются в формате JSON:
const pubJSON = sjcl.codec.base64.fromBits(publicKey.get());
На сервере необходимо строго соответствовать тому же формату, иначе вычисленный секрет будет различаться.
Общий секрет не должен использоваться напрямую, поскольку:
Для приведения к криптографически стойкому виду применяется схема:
Пример:
const shared = clientKeys.sec.derive(serverPub);
const masterKey = sjcl.hash.sha256.hash(shared);
const encKey = masterKey.slice(0, 4);
const macKey = masterKey.slice(4, 8);
Для предотвращения повторного использования ключей в разных сессиях вводится контекст:
Комбинированный вход в KDF:
const context = sjcl.bitArray.concat(
sharedSecret,
sjcl.codec.utf8String.toBits(sessionId)
);
const sessionKey = sjcl.hash.sha256.hash(context);
ECDH сам по себе не защищает от атак типа “man-in-the-middle”. Для устранения этой проблемы используется дополнительная аутентификация:
Пример проверки подписи:
const isValid = sjcl.ecc.verify(serverPub, signature, serverCertPub);
Использование разных параметров ECC приводит к несовместимости секрета.
Снижает стойкость и нарушает forward secrecy.
Без KDF приводит к предсказуемым AES ключам.
Любое изменение порядка байтов приводит к расхождению результата.
После согласования ключей формируется симметричный канал:
const aes = new sjcl.cipher.aes(sessionKey);
const ciphertext = sjcl.mode.gcm.encrypt(aes, plaintext, iv);
const plaintext = sjcl.mode.gcm.decrypt(aes, ciphertext, iv);
Для диагностики используют:
const fingerprint = sjcl.codec.hex.fromBits(
sjcl.hash.sha256.hash(sharedSecret)
);
Совпадение отпечатков подтверждает корректное согласование ключей между клиентом и сервером.