Перейти к содержанию
HexaTransfer
Вернуться к блогу
Шифрование и безопасность

Руководство по реализации 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 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.

Отправить файл