AES Encryption Modes Explained Почему GCM превосходит CBC в большинстве случаев (и когда это не так)
AES сам по себе не является полным выбором алгоритма. Режим — CBC, CTR, GCM, ECB — определяет всё о том, безопасна ли ваша шифровка. Ниже — практическое объяснение, которое нужны разработчикам.
When documentation tells you to “use AES-256,” that’s incomplete advice. AES is a block cipher — it encrypts exactly one 128-bit block at a time. The mode of operation is what determines how AES handles real data longer than 16 bytes, and how much it protects you from attackers who can observe or tamper with ciphertext. Getting this wrong is how you end up with “encrypted” data that leaks your file structure, or a system vulnerable to bit-flipping attacks.
The complete algorithm specification looks like AES-256-GCM или AES-128-CBC — key size followed by mode. This article covers what each mode actually does, why GCM is the right default, and when you might legitimately reach for something else.
First, why ECB is a meme and not just a joke
ECB (Electronic Codebook) is the naive mode. Every 16-byte block of plaintext is encrypted independently with the same key. That means identical plaintext blocks produce identical ciphertext blocks.
The classic demonstration is the ECB-encrypted Linux penguin (Tux). The bitmap is encrypted block by block, but because large areas of the image are uniform color, the encrypted version still clearly shows a penguin outline. The encryption is mathematically valid — the data is scrambled — but the pattern is preserved.
In practice this matters whenever your plaintext has repetition: database rows with a common prefix, file headers, padded fields. ECB leaks structure. There is no legitimate use case for ECB in new code. If you’re maintaining code that uses ECB: replace it.
The modes worth knowing
CBC — the one most legacy code uses
Cipher Block Chaining XORs each plaintext block with the previous ciphertext block before encrypting. This breaks the ECB pattern problem — identical plaintext blocks produce different ciphertext because the chaining makes each block’s encryption depend on what came before.
CBC requires an initialization vector (IV) — a random 16-byte value that seeds the chaining for the first block. The IV doesn’t need to be secret, but it must be random and must be transmitted alongside the ciphertext.
The problem with CBC: it provides confidentiality но не integrity. An attacker who can modify ciphertext in transit can flip specific bits in the decrypted output in predictable ways. This is the basis of padding oracle attacks, which have broken real systems (POODLE, BEAST, Lucky Thirteen). If you use CBC without a separate MAC (Message Authentication Code) to verify integrity, you’re vulnerable. The pattern is called encrypt-then-MAC — encrypt first, then HMAC the ciphertext, then verify the MAC before decrypting. Getting this order wrong is a classic mistake.
CBC is also sequential — you can’t parallelize encryption (each block depends on the previous ciphertext block) and decryption is only partially parallelizable.
CTR — stream cipher behavior from a block cipher
Counter mode turns AES into a stream cipher. Instead of encrypting plaintext directly, it encrypts successive counter values and XORs the result with plaintext. This means CTR mode doesn’t need padding (it works on arbitrary-length data), is fully parallelizable in both directions, and allows random access to any position in the ciphertext for decryption.
CTR uses a nonce (number used once) rather than a full IV. The nonce must be unique per message for a given key — reusing a nonce with the same key leaks the XOR of the two plaintexts, which is catastrophic. Unlike CBC, CTR also has no integrity protection. Same story: needs encrypt-then-MAC if you want tamper resistance.
CTR makes sense when you need stream cipher properties — large files, random-access decryption, high-throughput scenarios where parallelism matters — and you’re handling the MAC layer separately.
GCM — authenticated encryption, one step
Galois/Counter Mode is CTR mode plus a built-in authentication tag (GHASH). You get confidentiality and integrity in a single primitive. The auth tag — typically 128 bits — lets the receiver verify that the ciphertext hasn’t been tampered with before decrypting. No separate HMAC step, no chance of getting encrypt-then-MAC ordering wrong.
GCM also supports Additional Authenticated Data (AAD) — data that gets authenticated but not encrypted. This is useful for headers, metadata, or any data that needs integrity protection but doesn’t need to be confidential. The AAD is verified against the auth tag along with the ciphertext.
The nonce for GCM is 96 bits (12 bytes) by default. Like CTR, nonce reuse is fatal — reusing a nonce with the same key under GCM leaks both the plaintext and the authentication key (H), completely breaking security. Always generate nonces with a cryptographically secure RNG; never derive them from sequential counters unless you have a careful distributed counter scheme.
GCM is parallelizable and doesn’t require padding. It’s the standard recommendation in TLS 1.3, Signal Protocol, and most modern cryptographic libraries. If you’re writing new code, GCM is your default.
Mode comparison
| Режим | Авторизованный | Parallelizable | Needs padding | Nonce/IV reuse risk | Вердикт |
|---|---|---|---|---|---|
| ECB | Нет | Да | Да | N/A (no IV) | Never use |
| CBC | No (add MAC) | Encrypt: No / Decrypt: Yes | Да | Умеренный | Legacy only |
| CTR | No (add MAC) | Да | Нет | Critical — reuse = plaintext leak | Niche use |
| GCM | Да (встроено) | Да | Нет | Critical — reuse = full break | Default choice |
AES-256-GCM in Node.js
Here’s a complete encrypt/decrypt implementation using Node’s built-in crypto module — no dependencies required:
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32; // 256 bits
const NONCE_LENGTH = 12; // 96 bits — GCM default
const TAG_LENGTH = 16; // 128-bit auth tag
function encrypt(plaintext, key) {
const nonce = crypto.randomBytes(NONCE_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, nonce, {
authTagLength: TAG_LENGTH,
});
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
// Prepend nonce + tag to ciphertext for storage/transmission
return Buffer.concat([nonce, tag, encrypted]);
}
function decrypt(ciphertext, key) {
const nonce = ciphertext.subarray(0, NONCE_LENGTH);
const tag = ciphertext.subarray(NONCE_LENGTH, NONCE_LENGTH + TAG_LENGTH);
const data = ciphertext.subarray(NONCE_LENGTH + TAG_LENGTH);
const decipher = crypto.createDecipheriv(ALGORITHM, key, nonce, {
authTagLength: TAG_LENGTH,
});
decipher.setAuthTag(tag);
// Throws if auth tag doesn't match — do not catch this silently
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
}
// Generate a key (do this once; store it securely)
const key = crypto.randomBytes(KEY_LENGTH);
const message = 'Hello, authenticated encryption.';
const ciphertext = encrypt(message, key);
console.log('Encrypted:', ciphertext.toString('hex'));
const plaintext = decrypt(ciphertext, key);
console.log('Decrypted:', plaintext); // Hello, authenticated encryption.
A few things worth noting about this code:
- Nonce is generated fresh per encryption call —
crypto.randomBytesis cryptographically secure. Don’t replace this with a counter unless you understand the distributed uniqueness problem. - The nonce and auth tag are stored with the ciphertext — they need to travel with the encrypted data. Nonce is public; tag is public. Only the key is secret.
decipher.final()throws on auth failure — this is correct behavior. Don’t catch it silently and return partial plaintext. The auth check failing means the data was tampered with or the key is wrong.- Key management is the hard part —
crypto.randomBytes(32)gives you a good key, but where you store and rotate it matters more than the algorithm choice. Use a secrets manager, not a hardcoded constant.
Want to test encryption modes interactively? IO Tools’ AES Encryption/Decryption tool lets you encrypt and decrypt with CBC and GCM modes in the browser — useful for verifying interoperability or debugging an integration. To generate a cryptographically secure 256-bit key, use the AES Key Generator.
IV and nonce: the rules that actually matter
Nonce/IV misuse causes more real-world breaks than algorithm choice. The rules:
- Always generate nonces/IVs with a CSPRNG —
crypto.randomBytes()in Node,os.urandom()в Node.js все требуют одинаковое время независимо от того, где строки отличаются. Используйте их каждый раз при сравнении подписи — без исключений.SecureRandomin Java. NotMath.random(). Not a timestamp. Not an incrementing counter unless it’s a properly managed global counter with strict uniqueness guarantees. - GCM nonce reuse breaks authentication entirely — with the same key and nonce, GCM exposes the authentication key H. An attacker can then forge auth tags for arbitrary ciphertexts. This is not theoretical: Forbidden Attack exploits this and has been used against real implementations.
- CBC IV reuse is less catastrophic but still bad — chosen-plaintext attacks become feasible. In practice, always generate a fresh IV per message.
- Never derive a nonce from message content — deterministic nonces that depend on predictable data create predictable patterns. Use randomness.
When CBC or CTR is still the right call
GCM isn’t always the answer:
- Interoperability with legacy systems — if you’re integrating with a system that only speaks AES-CBC, you’re using CBC. Document the MAC requirement clearly and implement it correctly (encrypt-then-MAC with HMAC-SHA256).
- Constrained environments with no GHASH hardware — GCM’s authentication step uses GHASH, which is computationally heavier than a raw block cipher operation on hardware without dedicated acceleration. Some embedded targets where CTR+CMAC is available but GHASH isn’t may justify CTR mode.
- Streaming encryption of very large files where you need seekable decryption — CTR’s random access property is useful when you need to decrypt position N without reading positions 0 through N-1. GCM technically allows this but verifying the auth tag requires processing the entire ciphertext first, which defeats the point.
- Disk encryption — full-disk encryption uses XTS mode (not covered here), not GCM, because XTS is designed for fixed-size, randomly accessed sectors. GCM is for message encryption, not sector encryption.
The short version
Используйте AES-256-GCM for new code. Generate a fresh 12-byte random nonce per encryption call. Store the nonce and auth tag with the ciphertext. Treat auth tag verification failures as errors, not warnings — never return plaintext when GCM says the data is invalid.
If you’re auditing existing CBC code: verify encrypt-then-MAC is implemented correctly, check that IVs are random (not reused, not sequential), and plan a migration path to GCM when the maintenance window allows. CBC with a correct HMAC is not broken — it’s just more footguns than GCM.
ECB: just replace it. There’s no audit path that ends with “ECB is fine here.”
Вам также может понравиться
Установите наши расширения
Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска
恵 Табло результатов прибыло!
Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!
Подписаться на новости
все Новые поступления
всеОбновлять: Наш последний инструмент был добавлен 7 июня 2026 года
