Web Crypto API предоставляет только низкоуровневые криптографические примитивы: генерацию ключей, симметричное и асимметричное шифрование, хеширование, подписи и работу с ключевым материалом. Он не реализует готовые протоколы безопасности. Любая реальная система — TLS-подобное соединение, end-to-end шифрование сообщений, защищённое хранение токенов — строится как композиция этих примитивов.
Криптографический протокол в этом контексте — это формализованный набор шагов, определяющий:
Web Crypto API становится строительным материалом, а протокол — архитектурой поверх него.
Перед построением протоколов важно учитывать фундаментальные ограничения API:
Web Crypto API не хранит контекст соединения. Он не знает:
Все состояния должны поддерживаться на уровне приложения.
API не передаёт данные по сети. Он работает только с буферами:
ArrayBufferTypedArrayЛюбой протокол обязан самостоятельно определять формат сообщений и их передачу.
Нет реализации Diffie-Hellman handshake как процесса. Есть лишь примитивы:
deriveKeygenerateKeyimportKeyЛогика обмена ключами строится вручную.
Любой криптографический протокол поверх Web Crypto API опирается на несколько ключевых операций.
Чаще всего используется AES-GCM:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
AES-GCM одновременно обеспечивает:
Это делает его стандартным выбором для защищённых каналов.
Типичный выбор — ECDH:
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey"]
);
ECDH позволяет двум сторонам получить общий секрет без передачи ключа напрямую.
const hash = await crypto.subtle.digest("SHA-256", data);
Используется для:
const key = await crypto.subtle.importKey(
"raw",
secret,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
HMAC применяется в протоколах для защиты от подделки сообщений без использования асимметрии.
Одним из базовых криптографических протоколов является установление общего секрета между клиентом и сервером.
Каждая сторона создаёт свою пару:
const clientKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey"]
);
const serverKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey"]
);
Публичные ключи экспортируются:
const clientPublic = await crypto.subtle.exportKey(
"raw",
clientKeyPair.publicKey
);
Передача происходит по открытому каналу.
Каждая сторона использует свой приватный ключ и публичный ключ собеседника:
const sharedKey = await crypto.subtle.deriveKey(
{
name: "ECDH",
public: serverPublicKey
},
clientKeyPair.privateKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
Результат — симметричный ключ, который нельзя восстановить из перехваченного трафика без приватных ключей.
После установки общего секрета строится сессионный протокол.
Типичная структура:
const nonce = crypto.getRandomValues(new Uint8Array(12));
Nonce должен быть уникальным для каждого сообщения в рамках одного ключа.
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce
},
sessionKey,
plaintext
);
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: nonce
},
sessionKey,
encrypted
);
Для предотвращения подмены сообщений часто используется HMAC поверх шифрования.
const mac = await crypto.subtle.sign(
"HMAC",
hmacKey,
ciphertext
);
const valid = await crypto.subtle.verify(
"HMAC",
hmacKey,
mac,
ciphertext
);
Если проверка не проходит, сообщение отклоняется до расшифрования.
Web Crypto API не предоставляет встроенной защиты от replay-атак, поэтому она реализуется на уровне протокола.
Каждое сообщение содержит индекс:
{
"seq": 42,
"payload": "..."
}
Протокол хранит последний принятый индекс:
seq <= lastSeq → сообщение отклоняетсяseq > lastSeq → обновление состоянияАсимметричные подписи используются для независимой проверки отправителя.
const keyPair = await crypto.subtle.generateKey(
{ name: "Ed25519" },
true,
["sign", "verify"]
);
const signature = await crypto.subtle.sign(
"Ed25519",
privateKey,
data
);
const isValid = await crypto.subtle.verify(
"Ed25519",
publicKey,
signature,
data
);
Подписи используются в протоколах:
На практике почти все современные протоколы комбинируют несколько криптографических подходов.
const derivedKey = await crypto.subtle.deriveKey(
{
name: "HKDF",
salt,
info,
hash: "SHA-256"
},
sharedSecret,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
HKDF позволяет избежать использования сырых ECDH-значений напрямую.
Частый сценарий — защищённый WebSocket-канал.
{
"iv": "...",
"data": "...",
"tag": "...",
"seq": 10
}
При построении протоколов на Web Crypto API часто возникают критические ошибки:
В AES-GCM это полностью разрушает безопасность.
Шифрование без аутентификации делает систему уязвимой к подмене.
RSA-OAEP или SHA-1 в современных протоколах создают слабые места.
Даже идеально зашифрованные сообщения можно повторно отправить.
Поверх Web Crypto API всегда существует абстракция:
Web Crypto API занимает только второй уровень и не заменяет протоколы безопасности.
Любая реализация должна учитывать:
Криптографические протоколы поверх Web Crypto API представляют собой слой инженерной логики, который превращает набор математических операций в систему безопасного взаимодействия между участниками.