Сервис шифрует каждую запись AEAD-шифром AES-GCM с одним фиксированным IV — что ломается?
Сервис шифрует поля базы данных через AES-256-GCM. Ниже фрагмент его конфигурации и три строки из аудит-лога.
Ограничения: шифр должен остаться AEAD; исправление должно переживать перезапуск процесса и работу нескольких писателей одновременно; уже сохранённый шифротекст должен остаться расшифровываемым на время плана миграции.
crypto.algorithm = aes-256-gcm
crypto.key = ${DATA_KEY} # один ключ на всё окружение
crypto.iv = 000102030405060708090a0b # константа из конфига
crypto.aad =
audit encrypt rec=1041 nonce=000102030405060708090a0b tag=9f3c1a...
audit encrypt rec=1042 nonce=000102030405060708090a0b tag=4b17e0...
audit encrypt rec=1043 nonce=000102030405060708090a0b tag=c2d95b...
Объясните, какое свойство защиты рушится, и исправьте конфигурацию.
Повтор nonce в GCM под одним ключом катастрофичен, а не просто нежелателен: поток шифрования повторяется, утекают соотношения между текстами, а подключ аутентификации становится восстановимым — целостность теряется для всего ключа. Нужен уникальный nonce и новый ключ.
- ✗Считать повтор nonce мелкой потерей энтропии, а не отказом на уровне всего ключа
- ✗Думать, что более длинный ключ компенсирует переиспользованный nonce
- ✗Полагать, что nonce влияет только на тег аутентификации, но не на поток шифрования
- →Какие стратегии генерации nonce остаются безопасными при перезапусках и нескольких писателях?
- →Что меняет в этом отказе режим, устойчивый к неправильному использованию nonce?
Что рушится
GCM — это режим счётчика плюс аутентификатор. Из ключа и nonce выводится поток шифрования, поэтому один и тот же nonce под одним ключом даёт один и тот же поток: записи, зашифрованные с ним, оказываются связаны между собой, и конфиденциальность для этой группы записей перестаёт держаться. Хуже того, повтор nonce делает восстановимым подключ аутентификации GCM, а это лишает целостности все сообщения под данным ключом — теги перестают быть доверенными.
Вывод: скомпрометирован не отдельный документ, а весь ключ. Поэтому исправление состоит из двух частей — уникальный nonce и новый ключ.
Исправление
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# Ключ приходит из KMS/секрет-хранилища, а не из конфига; старый ключ считается
# скомпрометированным и остаётся только для расшифровки на время миграции.
def encrypt_record(key: bytes, record_id: str, plaintext: bytes) -> bytes:
nonce = os.urandom(12) # уникальный nonce на каждую запись
aad = record_id.encode() # привязка шифротекста к его записи
ciphertext = AESGCM(key).encrypt(nonce, plaintext, aad)
return nonce + ciphertext # nonce хранится рядом с шифротекстом
Ключевые моменты: crypto.iv из конфигурации удаляется полностью — nonce не настраивается, а генерируется на каждое сообщение и сохраняется вместе с шифротекстом; 96-битный случайный nonce безопасен при перезапусках и нескольких писателях; данные перешифровываются под новым ключом, а старый выводится из обращения.