Руководство по реализации AES-GCM: правильное аутентифицированное шифрование
Правильно реализуйте шифрование AES-GCM. Управление nonce, ключами и типичные ошибки аутентифицированного шифрования.
AES-GCM (Galois/Counter Mode) сочетает шифрование AES-CTR с аутентификацией GHASH для создания аутентифицированного шифрования со связанными данными (AEAD). Корректная реализация использует 256-битный ключ, 96-битный (12-байтный) nonce, уникальный для каждого ключа (никогда не повторяемый), 128-битный тег аутентификации и опционально аутентифицированные связанные данные (AAD), которые проверяются, но не шифруются. Точную конструкцию задаёт NIST SP 800-38D. Любая ошибка, особенно повторное использование nonce, катастрофически нарушает безопасность GCM: единственная повторяющаяся пара (ключ, nonce) позволяет атакующим восстановить ключ аутентификации и подделать произвольные шифротексты. Это руководство описывает корректное использование AES-GCM в браузере, Node.js и серверных контекстах.
Что гарантирует GCM
Два свойства:
Конфиденциальность: открытый текст не может быть восстановлен без ключа. Уровень шифрования CTR в AES-GCM обеспечивает это.
Целостность и подлинность: любое изменение шифротекста, nonce или связанных данных приводит к сбою дешифрования. GHASH производит 128-битный тег, который проверяется с постоянным временем при дешифровании.
Чего GCM не гарантирует: неотрекаемость (алгоритм симметричный, поэтому любой, кто имеет ключ, может создать корректные шифротексты), защита от повторного воспроизведения (это задача верхнего уровня) или упорядочивание (для потоков нужна связка как-то иначе).
Ключевое понимание: GCM остаётся безопасным только при уникальных nonce для каждого ключа. Не в большинстве случаев, не обычно — строго уникальных. Доказательство безопасности разрушается при повторном использовании.
Управление nonce: самое важное
96-битный nonce можно генерировать двумя способами:
Случайный: crypto.getRandomValues(new Uint8Array(12)). При случайных 96-битных nonce под одним ключом коллизии по граничному условию парадокса дней рождений появляются примерно после 2^48 шифрований. NIST предлагает запас безопасности, поэтому ограничивайте использование до 2^32 на ключ.
Счётчик: инкрементируйте 96-битное целое число. Гарантирует уникальность до 2^96 сообщений. Требует надёжного монотонного состояния, что сложно в распределённых системах.
Для передачи файлов со свежим ключом на каждый файл случайные nonce вполне безопасны — вы никогда не достигнете 2^32 шифрований под одним ключом. Для чанкованного шифрования под одним ключом файла используйте счётчик, где nonce кодирует индекс чанка:
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));
Катастрофический сценарий: несколько процессов, шифрующих под одним общим ключом с случайными nonce, масштабируясь до миллионов шифрований в секунду. Коллизии дней рождений становятся вероятными. Если необходимо разделять ключи между процессами, используйте координированный счётчик с префиксом идентификатора процесса.
Не используйте 64-битные nonce
AES-GCM поддерживает переменную длину nonce, но только 96-битные nonce используют оптимизированную конструкцию, указанную в NIST 800-38D. Другие длины (как правило, 64 или 128 бит) запускают этап предварительной обработки GHASH, снижающий производительность и усложняющий реализацию. Web Crypto API принимает не-96-битные IV, но спецификация рекомендует 96. Просто используйте 96.
Длина тега: не укорачивайте
Тег GCM достигает 128 бит. Некоторые спецификации допускают усечение до 96, 64 или даже 32 бит. Не делайте этого. Усечённые теги упрощают атаки подделки, а экономия (4-12 байт на сообщение) несущественна для передачи файлов. Параметр AES-GCM в Web Crypto по умолчанию использует 128-битные теги через параметр tagLength (значение по умолчанию 128). Не меняйте его.
Связанные данные (AAD)
AAD — данные, которые аутентифицируются, но не шифруются. Используйте их для метаданных, которые должны быть привязаны к шифротексту: имя файла, тип содержимого, временная метка истечения, идентификатор загрузчика.
await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
additionalData: new TextEncoder().encode(JSON.stringify({
filename: "report.pdf",
contentType: "application/pdf",
expires: 1712345678,
})),
},
key,
plaintext
);
Если атакующий изменяет AAD, дешифрование завершается неудачей. Это предотвращает атаки подмены, при которых кто-то заменяет имя файла в сохранённом шифротексте незаметно. Получатель должен знать точные AAD для дешифрования, поэтому храните их рядом с шифротекстом.
Генерация и деривация ключей
Для ключей на каждый файл:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
256 бит — стандарт 2026 года. 128-битный AES по-прежнему безопасен, но имеет меньший постквантовый запас (алгоритм Гровера вдвое сокращает эффективную стойкость).
Для ключей, производных от пароля:
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
Храните соль рядом с шифротекстом. Она не является секретом; просто должна быть уникальной для каждого пароля.
Критический путь кода
Минимальная функция шифрования:
async function encrypt(key, plaintext, aad = new Uint8Array()) {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
plaintext
)
);
return { nonce, ciphertext, aad };
}
Дешифрование с корректной обработкой ошибок:
async function decrypt(key, { nonce, ciphertext, aad }) {
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
ciphertext
);
} catch (e) {
// Ошибка аутентификации
throw new Error("Дешифрование не удалось: шифротекст подделан или неверный ключ");
}
}
Вызов decrypt выбрасывает OperationError при несоответствии тега, слишком коротком шифротексте или неверном ключе. Любое исключение трактуйте как нарушение целостности; не пытайтесь различать причины.
Чанкованные большие файлы
Для файлов свыше нескольких сотен мегабайт разбивайте на чанки, чтобы избежать давления на память:
async function encryptChunks(key, file, chunkSize = 1024 * 1024) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = await file.slice(offset, offset + chunkSize).arrayBuffer();
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce }, key, chunk
);
chunks.push(new Uint8Array(ct));
}
return chunks;
}
Предостережение: чанкованный AES-GCM не обнаруживает усечение. Атакующий может отбросить завершающие чанки, и каждый оставшийся чанк дешифруется корректно. Для защиты включите общее количество чанков в AAD каждого чанка, или используйте crypto_secretstream из libsodium, который обрабатывает это.
Дешифрование на стороне сервера (Node.js)
Модуль crypto Node может дешифровать данные, зашифрованные в браузере:
const { createDecipheriv } = require('crypto');
function decrypt(key, nonce, ciphertextWithTag) {
const tag = ciphertextWithTag.slice(-16);
const ct = ciphertextWithTag.slice(0, -16);
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]);
}
Web Crypto добавляет 128-битный тег к шифротексту; API Node ожидает тег и шифротекст раздельно. Разделяйте соответственно.
Числа производительности
На типичном оборудовании 2024-2026 годов с AES-NI:
- Нативный (OpenSSL, AES-NI): 3-5 ГБ/с на ядро
- Web Crypto (браузер с аппаратным ускорением): 1-2 ГБ/с
- libsodium.js WASM AES-GCM: 400-800 МБ/с
- Чистый JS (@noble/ciphers): 50-150 МБ/с
Для файла 1 ГБ шифрование через Web Crypto занимает 0.5-1 секунду. Чистый JS — 7-20 секунд. Выбирайте реализации с учётом этой реальности; для UX передачи больших файлов Web Crypto — практичный выбор.
Итоговые подводные камни
- Повторное использование nonce: катастрофично. Самый серьёзный режим отказа.
- Использование
Math.random()вместоcrypto.getRandomValues(). - Отсутствие аутентификации связанных метаданных через AAD.
- Применение режима CBC «по привычке». CBC требует отдельного MAC для обеспечения целостности, сопоставимой с GCM; конструкция HMAC-CBC корректна, но сложна, GCM устраняет проблему.
- Молчаливый перехват ошибок дешифрования и возврат мусора. Всегда завершайте с ошибкой явно.
- Написание собственного GCM. Используйте Web Crypto, libsodium или node:crypto. Реализация GHASH имеет побочные каналы по времени, которые эксперты исправляли годами.
HexaTransfer использует AES-256-GCM Web Crypto с 96-битными случайными nonce, 128-битными тегами и без AAD, поскольку ключ уникален для файла, а имя файла хранится отдельно в метаданных, защищённых AEAD. Просто, корректно, быстро.
Когда выбрать что-то другое
AES-GCM оптимален для передачи файлов, но рассмотрите альтернативы в конкретных случаях:
- XChaCha20-Poly1305: 192-битные nonce делают безопасность случайных nonce тривиальной при любом масштабе. Немного медленнее на оборудовании с AES-NI, быстрее на старых ARM без AES-NI. Предоставляется libsodium.
- AES-GCM-SIV: устойчив к неправильному использованию; повторное использование nonce не раскрывает ключ, а лишь показывает, совпадают ли открытые тексты. Полезен, когда нельзя гарантировать уникальность nonce.
Для большинства рабочих нагрузок передачи файлов в стандартном веб-стеке AES-256-GCM со свежим ключом на каждый файл и случайными 96-битными nonce — правильный выбор и наиболее простой в корректной реализации.
Попробуйте на hexatransfer.com — бесплатно, без регистрации, до 10 ГБ.
Безопасная отправка больших файлов со сквозным шифрованием
Передавайте файлы до 10 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.
Отправить файл