Шифрование AES (Advanced Encryption Standard) работает с данными блоками по 128 бит, то есть по 16 байт, а ключ использует длиной 128, 192 или 256 бит. Тот же секретный ключ и шифрует, и расшифровывает, поэтому AES относится к симметричным алгоритмам блочного шифрования. Каждый блок проходит 10, 12 или 14 раундов шифрования в зависимости от длины ключа.
Все примеры ниже прогнаны на Python 3.14.3 с библиотекой cryptography 50.0.2 (pip install cryptography), каждый блок запускается сам по себе, вывод скопирован из консоли.
- Что такое AES и откуда он взялся
- AES-128, AES-192 и AES-256: в чём разница
- Как работает алгоритм шифрования AES
- SubBytes: замена байтов по таблице S-box
- ShiftRows: циклический сдвиг строк
- MixColumns: перемешивание столбцов
- AddRoundKey: наложение раундового ключа
- Расширение ключа: откуда берутся раундовые ключи
- Расшифровка: алгоритм дешифрования AES
- Проверка AES по тестовому примеру стандарта
- Режимы шифрования AES
- ECB: почему этот режим нельзя использовать
- CBC: сцепление блоков и вектор инициализации
- CTR: счётчик вместо сцепления
- GCM: шифрование с проверкой целостности
- Пример: AES-256-GCM с ключом из пароля
- Что будет, если повторить nonce
- Где используется AES
- Безопасен ли алгоритм AES
- Частые вопросы об AES
- Можно ли расшифровать AES без ключа?
- AES симметричный или асимметричный алгоритм?
- Чем шифрование AES отличается от хеширования?
- Можно ли шифровать AES онлайн?
Что такое AES и откуда он взялся
В 1997 году в США объявили открытый конкурс на новый стандарт шифрования: блочные шифры мог предложить кто угодно. В 2000 году в конкурсе победил алгоритм Rijndael, его авторы Йоан Даймен и Винсент Рэймен (Joan Daemen, Vincent Rijmen). Стандарт AES опубликовали 26 ноября 2001 года.
До AES стандартом шифрования был DES (Data Encryption Standard) 1977 года: блок 64 бита, ключ из 64 бит, из которых алгоритм использует 56, а остальные 8 служат для контроля ошибок. В 2005 году DES отозвали, потому что он больше не давал нужного уровня защиты.
AES-128, AES-192 и AES-256: в чём разница
Варианты алгоритма отличаются тремя вещами: длиной ключа, количеством раундов и правилами, по которым из ключа получают раундовые ключи. Размер блока у всех один.
| Вариант | Длина ключа | Слов в ключе | Блок | Раундов | Раундовых ключей | Слов в расписании |
|---|---|---|---|---|---|---|
| AES-128 | 128 бит (16 байт) | 4 | 128 бит | 10 | 11 | 44 |
| AES-192 | 192 бита (24 байта) | 6 | 128 бит | 12 | 13 | 52 |
| AES-256 | 256 бит (32 байта) | 8 | 128 бит | 14 | 15 | 60 |
Слово здесь занимает 4 байта. Раундовых ключей на один больше, чем раундов: первый накладывается ещё до первого раунда. Чем длиннее ключ, тем больше вариантов при полном переборе: 2^128 у AES-128 и 2^256 у AES-256.
В новом коде берите ключ 256 бит: запас по перебору больше, а в Python это тот же вызов AESGCM.generate_key(bit_length=256), только с другим числом.
Как работает алгоритм шифрования AES
Алгоритм использует 16 байт блока как матрицу 4×4, её называют состоянием (state). Байты входа заполняют матрицу по столбцам: байты 0-3 становятся левым столбцом, 4-7 следующим. Дальше алгоритм делает так:
- Складывает состояние с первым раундовым ключом (AddRoundKey).
- Во всех раундах, кроме последнего, выполняет SubBytes, ShiftRows, MixColumns и AddRoundKey.
- В последнем раунде выполняет SubBytes, ShiftRows и AddRoundKey, без MixColumns.
SubBytes: замена байтов по таблице S-box
Каждый байт независимо от других заменяется по таблице S-box из 256 значений (16×16). Таблица построена так: сначала берётся мультипликативно обратный элемент байта в конечном поле GF(2^8), затем к результату применяется аффинное преобразование. Например, байт 53 превращается в ed. Преобразование обратимое: при расшифровке работает обратная таблица. Вся нелинейность AES сосредоточена в этой таблице, и её выбирали так, чтобы затруднить линейный и дифференциальный криптоанализ.
ShiftRows: циклический сдвиг строк
Ряды 1-3 сдвигаются влево по кругу: ряд 1 на 1 байт, ряд 2 на 2, ряд 3 на 3, а ряд 0 остаётся на месте.

MixColumns: перемешивание столбцов
Каждый столбец умножается на фиксированную матрицу: первая её строка 02 03 01 01, остальные получаются циклическим сдвигом вправо. Умножение и сложение идут в поле GF(2^8) из 256 элементов, где сложение байтов выполняется с помощью операции XOR.
AddRoundKey: наложение раундового ключа
Состояние складывается с раундовым ключом с помощью XOR: каждый из четырёх столбцов с одним словом ключа. Раундовый ключ того же размера, что и блок: 16 байт. Повторный XOR с тем же ключом возвращает исходное значение, поэтому операция AddRoundKey обратна сама себе.
Расширение ключа: откуда берутся раундовые ключи
Процедура KeyExpansion превращает ключ шифрования в расписание из 4 x (Nr + 1) слов, где Nr означает число раундов: 44 слова у AES-128, 52 у AES-192 и 60 у AES-256. Начало расписания, Nk слов (4, 6 или 8), повторяет сам ключ. Каждое следующее слово получается XOR предыдущего слова и слова, стоящего на Nk позиций раньше.
Если номер слова делится на Nk, предыдущее слово перед XOR проходит три операции: RotWord сдвигает его байты по кругу на один, SubWord заменяет каждый байт по той же таблице S-box, затем добавляется раундовая константа Rcon. Констант десять: 01 02 04 08 10 20 40 80 1b 36. AES-128 использует все десять, AES-192 первые восемь, AES-256 первые семь. У AES-256 есть ещё одно правило: если номер слова плюс 4 делится на 8, предыдущее слово перед XOR проходит только SubWord, без сдвига и константы.
Расшифровка: алгоритм дешифрования AES
Алгоритм выполняет обратные преобразования в обратном порядке. Сначала состояние складывается с последним раундовым ключом. Затем раунды идут от предпоследнего к первому, в каждом четыре шага: InvShiftRows сдвигает ряды вправо, InvSubBytes подставляет байты по обратной таблице, потом AddRoundKey и InvMixColumns. Завершают расшифровку InvShiftRows, InvSubBytes и AddRoundKey с первым раундовым ключом, без InvMixColumns. Сложений с ключом снова Nr + 1.
Проверка AES по тестовому примеру стандарта
В тексте стандарта есть пример работы алгоритма: ключ, блок открытого текста и шифротекст, который должен получиться. Здесь режим ECB нужен как «голое» преобразование одного блока, для данных он не годится.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# Ключ и блок из примера в тексте стандарта AES (AES-128)
standard_key = bytes.fromhex("2b7e151628aed2a6abf7158809cf4f3c")
plain_block = bytes.fromhex("3243f6a8885a308d313198a2e0370734")
encryptor = Cipher(algorithms.AES(standard_key), modes.ECB()).encryptor()
cipher_block = encryptor.update(plain_block) + encryptor.finalize()
print(cipher_block.hex())
print(cipher_block.hex() == "3925841d02dc09fbdc118597196a0b32")
3925841d02dc09fbdc118597196a0b32
True
Так проверяют чужую реализацию или свою учебную; для AES-192 и AES-256 берут их собственные примеры. Если Python пишет ModuleNotFoundError: No module named 'cryptography', пакет не установлен в то окружение, где запускается скрипт. Как это найти, разобрано в статье про ошибку No module named.

Режимы шифрования AES
Сам алгоритм AES шифрует ровно один блок из 16 байт. Данные большего размера шифруют в каком-либо режиме: он задаёт, как блоки связаны между собой и что делать с последним неполным блоком.
ECB: почему этот режим нельзя использовать
В ECB каждый блок шифруется отдельно одним и тем же ключом. Одинаковые блоки открытого текста при одном ключе всегда дают одинаковые блоки шифротекста. Проверим на сообщении из трёх блоков, где первые два совпадают:
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
standard_key = bytes.fromhex("2b7e151628aed2a6abf7158809cf4f3c") # ключ из стандарта
message = b"transfer=100rub;" * 2 + b"transfer=900rub;" # три блока по 16 байт
ecb = Cipher(algorithms.AES(standard_key), modes.ECB()).encryptor()
ecb_out = ecb.update(message) + ecb.finalize()
for start in range(0, len(ecb_out), 16):
print(start // 16 + 1, ecb_out[start:start + 16].hex())
print("ECB, блоки 1 и 2 равны:", ecb_out[:16] == ecb_out[16:32])
iv = os.urandom(16)
cbc = Cipher(algorithms.AES(standard_key), modes.CBC(iv)).encryptor()
cbc_out = cbc.update(message) + cbc.finalize()
print("CBC, блоки 1 и 2 равны:", cbc_out[:16] == cbc_out[16:32])
1 e4db00790f644eda01527b7f9887a5ba
2 e4db00790f644eda01527b7f9887a5ba
3 8e2553c15caed3087f744350bd854ccd
ECB, блоки 1 и 2 равны: True
CBC, блоки 1 и 2 равны: False
Ключ из стандарта здесь взят ради повторяемого вывода, в рабочем коде ключ случайный. Человек, который видит только шифротекст, не знает сумм, но видит, что первые два перевода одинаковые, а третий другой. В данных с повторяющимися участками такие совпадения оставляют в шифротексте заметные узоры. Если по шифротексту не должно быть видно повторов, а для пользовательских данных это так почти всегда, режим ECB не годится.
CBC: сцепление блоков и вектор инициализации

В CBC перед шифрованием каждый блок открытого текста складывается через XOR с предыдущим блоком шифротекста, а первый блок с вектором инициализации (IV). IV занимает 16 байт и может передаваться вместе с шифротекстом, но он должен быть непредсказуемым и новым для каждого сообщения. Постоянный IV выдаёт сведения о данных. Шифрование в CBC идёт строго по очереди, а расшифровка может работать параллельно.
CBC требует дополнения (padding) до целого числа блоков. Схема PKCS7 дописывает N байтов со значением N. Например, к 27 байтам PKCS7 допишет пять байтов 05, до 32. Если длина уже кратна 16, добавляется целый блок из 16 байтов 10: так получатель не срежет по ошибке последние байты сообщения, которому дополнение не требовалось.
CBC прячет содержимое, но подмены не замечает: злоумышленник может прислать изменённый шифротекст, и программа его расшифрует. Поэтому CBC руками не собирайте, для своих данных берите GCM.
CTR: счётчик вместо сцепления
В CTR алгоритм шифрует не сами данные, а последовательность блоков-счётчиков, и результат складывается с открытым текстом через XOR. Блочный шифр превращается в потоковый: дополнение не нужно, блоки шифруются и расшифровываются параллельно. При одном ключе все счётчики во всех сообщениях должны быть разными.
GCM: шифрование с проверкой целостности
Режим шифрования GCM использует CTR и добавляет тег аутентификации. Тег ловит и случайную порчу данных, и намеренную подмену. Nonce рекомендуется длиной 12 байт (96 бит). Класс AESGCM из cryptography дописывает к шифротексту тег длиной 16 байт и другие длины не поддерживает.
| Режим | Дополнение | IV или nonce | Проверка целостности | Параллельная обработка |
|---|---|---|---|---|
| ECB | да | нет | нет | да |
| CBC | да | 16 байт, непредсказуемый | нет | только расшифровка |
| CTR | нет | уникальный счётчик | нет | да |
| GCM | нет | 12 байт, уникальный | тег 16 байт | да |
Пример: AES-256-GCM с ключом из пароля
Пароль нельзя подставлять вместо ключа: алгоритм ждёт правильно сгенерированный ключ ровно 16, 24 или 32 байта. Ключ из пароля получают с помощью функции формирования ключа (KDF), этот процесс называют растяжением ключа. Ниже это Argon2id с параметрами из второго рекомендованного набора его стандарта: 64 МиБ памяти, 3 прохода, 4 потока, соль 16 байт. Это те же параметры, что в статье о хешировании паролей.
import os
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
def stretch_password(password: str, salt: bytes) -> bytes:
kdf = Argon2id(salt=salt, length=32, iterations=3, lanes=4,
memory_cost=64 * 1024)
return kdf.derive(password.encode())
def lock(password: str, note: str) -> bytes:
salt = os.urandom(16)
nonce = os.urandom(12) # новый nonce на каждое сообщение
key = stretch_password(password, salt)
ciphertext = AESGCM(key).encrypt(nonce, note.encode(), None)
return salt + nonce + ciphertext
def unlock(password: str, package: bytes) -> str:
salt, nonce, ciphertext = package[:16], package[16:28], package[28:]
key = stretch_password(password, salt)
return AESGCM(key).decrypt(nonce, ciphertext, None).decode()
package = lock("лето-на-даче-1987", "Отчёт за сентябрь готов")
print(len(package), "байт")
print(unlock("лето-на-даче-1987", package))
print(package != lock("лето-на-даче-1987", "Отчёт за сентябрь готов"))
broken = package[:-1] + bytes([package[-1] ^ 1])
checks = [("изменён 1 бит", "лето-на-даче-1987", broken),
("чужой пароль", "лето-на-даче-1988", package)]
for label, password, data in checks:
try:
unlock(password, data)
except InvalidTag:
print(label, "-> InvalidTag")
87 байт
Отчёт за сентябрь готов
True
изменён 1 бит -> InvalidTag
чужой пароль -> InvalidTag
Пакет состоит из соли (16 байт), nonce (12 байт), шифротекста (43 байта, столько занимает строка в UTF-8) и тега (16 байт). Соль и nonce не секретны, их хранят рядом с данными, иначе расшифровать не получится. Одинаковый текст с тем же паролем шифруется каждый раз по-новому: соль и nonce случайные, поэтому третья строка выводит True.
Изменение одного бита и неверный пароль дают одно и то же исключение InvalidTag, библиотека не возвращает мусор вместо текста. Ловите именно его: отличить по нему порчу данных от чужого пароля нельзя. Если пароля нет и ключ хранит сам сервер, Argon2id не нужен: возьмите AESGCM.generate_key(bit_length=256) и держите ключ вне кода и репозитория.

Что будет, если повторить nonce
Для шифрования GCM использует вариант режима CTR: шифротекст получается как открытый текст XOR поток, а поток зависит только от ключа и nonce. Если оба совпали у двух сообщений, XOR двух шифротекстов равен XOR двух открытых текстов. Зная одно сообщение, можно получить второе:
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12) # ОШИБКА: один nonce на два сообщения
first = b"login=admin;pass=Qwerty-2026"
second = b"login=guest;pass=Sunrise-777"
first_ct = aesgcm.encrypt(nonce, first, None)[:-16] # отрезаем 16 байт тега
second_ct = aesgcm.encrypt(nonce, second, None)[:-16]
recovered = bytes(a ^ b ^ c for a, b, c in zip(first_ct, second_ct, first))
print(recovered)
b'login=guest;pass=Sunrise-777'
Ключа программа не знала: ей хватило двух шифротекстов и одного известного текста. В GCM на этом потери не кончаются: по сообщениям с повтором злоумышленник, скорее всего, вычислит внутренний ключ проверки и сможет подделывать сообщения с правильным тегом. Поэтому на каждое сообщение вызывайте os.urandom(12) заново. За время жизни одного ключа со случайным nonce можно зашифровать не больше 2^32 сообщений, дальше ключ меняют.

Где используется AES
- В TLS 1.3: любое совместимое приложение обязано поддерживать набор TLS_AES_128_GCM_SHA256 и должно поддерживать TLS_AES_256_GCM_SHA384.
- В Wi-Fi: WPA2 включает AES, а старый TKIP придуман в 2004 году как временная мера для устройств, которые AES не тянули.
- В шифровании дисков: BitLocker по умолчанию использует XTS-AES со 128-битным ключом, 256 бит рекомендуют для более производительных дисков и процессоров.
- В процессорах x86 есть инструкции AES-NI; ядро Linux использует их (и векторные VAES) для AES в режимах ECB, CBC, CTR, XTS, GCM и других.

Безопасен ли алгоритм AES
Слабых ключей, как у DES, у AES практически не бывает, и ограничений на выбор правильно сгенерированного ключа нет. Проблемы безопасности появляются в коде вокруг алгоритма шифрования: режим ECB, повторный nonce, CBC без проверки целостности, пароль на месте ключа, ключ в исходнике, который уходит в репозиторий вместе с кодом.
Квантовые компьютеры не повод срочно менять AES-128. Алгоритм Гровера сокращает перебор квадратично, но полный выигрыш даёт только при последовательном выполнении шагов, а при параллельном переборе выигрыш меньше. По нынешним оценкам, AES-128 скорее всего останется стойким к таким атакам ещё десятилетия, а AES-192 и AES-256 тем более.
Свою реализацию раундов в рабочий проект не берите. Быстрый программный AES, который тратит одинаковое время на любые данные, на обычных процессорах написать очень трудно, а по разнице во времени ответов сетевого сервера ключ AES уже восстанавливали целиком. Тест из стандарта такие утечки не ловит.
Частые вопросы об AES
Можно ли расшифровать AES без ключа?
Без ключа остаётся полный перебор: до 2^128 вариантов у AES-128 и до 2^256 у AES-256. Если ключ получен из пароля, перебирают уже пароль, и короткий пароль даёт куда меньше вариантов.
AES симметричный или асимметричный алгоритм?
Симметричный: один секретный ключ и шифрует, и расшифровывает, поэтому обе стороны должны его знать. В асимметричных алгоритмах, например RSA, ключей два: публичный и закрытый.
Чем шифрование AES отличается от хеширования?
Шифротекст расшифровывается ключом обратно в исходные данные, у хеша обратной операции нет. Для паролей пользователей нужна медленная хеш-функция с солью. Подробнее в статье «Хеш-функция».
Можно ли шифровать AES онлайн?
Для учебных примеров можно: тестовый ключ из стандарта и открытый текст ничего не стоят. Рабочие ключи и данные в чужие сервисы не вставляйте, вы не проверите, что страница не отправляет их на сервер.








