This cheatsheet lists the encryption algorithms a developer actually chooses between, with exact key sizes, nonce sizes, tag lengths and current status. It is for anyone picking a cipher, reading a library error such as "Invalid key length", or checking whether DES or RC4 is still acceptable. The common confusion it clears up is that a cipher (AES) and a mode (GCM, CBC) are separate choices, and the mode decides whether tampering is detected.
| Algorithm | Key size | Block or stream | Status |
|---|---|---|---|
| AES-128, AES-192, AES-256 | 128, 192, 256 bits | 128-bit block | Current standard (FIPS 197) |
| ChaCha20-Poly1305 | 256 bits | Stream, 64-byte keystream blocks | Current, standardized in RFC 8439 |
| 3DES (Triple DES) | 168 bits nominal | 64-bit block | NIST disallowed encryption with it after Dec 31, 2023 |
| DES | 56 bits | 64-bit block | Broken, brute-forceable |
| RC4 | Variable | Stream | TLS clients and servers must never negotiate it (RFC 7465) |
| Mode | Nonce or IV | Tamper detection | Notes |
|---|---|---|---|
| GCM | 96 bits (12 bytes) recommended | Yes, 128-bit tag | Authenticated. Never reuse a nonce under one key |
| CBC | 128 bits (16 bytes), unpredictable | No | Needs padding. Output length is a multiple of 16 |
| CTR | 128-bit counter block | No | Stream-like, no padding, no integrity |
| ECB | None | No | Equal plaintext blocks give equal ciphertext blocks. Do not use |
| ChaCha20-Poly1305 | 96 bits (12 bytes) | Yes, 128-bit tag | Authenticated, fast without AES hardware |
| Algorithm | Purpose | Sizes | Notes |
|---|---|---|---|
| RSA-2048 | Encrypt small data, sign | 2048-bit modulus, 256-byte output | About 112 bits of security |
| RSA-3072 | Encrypt small data, sign | 3072-bit modulus, 384-byte output | About 128 bits of security |
| Ed25519 | Sign only | 32-byte keys, 64-byte signature | Not an encryption algorithm |
| X25519 | Key agreement | 32-byte keys, 32-byte shared secret | Derive a symmetric key from the secret |
| ML-KEM-512, 768, 1024 | Post-quantum key encapsulation | Three parameter sets | NIST FIPS 203, August 2024 |
For data at rest or in an API payload, use AES-256-GCM, or ChaCha20-Poly1305 where AES hardware is missing. For sending a secret to someone who has only your public key, use RSA-OAEP or an X25519 exchange. For proving who wrote something, sign with Ed25519 or RSA. Password storage is a different job entirely and uses a slow hash, not encryption.
const c = require('crypto');
const key = Buffer.alloc(32, 1), iv = Buffer.alloc(12, 2);
const x = c.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([x.update('attack at dawn'), x.final()]);
const tag = x.getAuthTag();
console.log(ct.length, 'bytes ciphertext,', tag.length, 'bytes tag');
const bad = Buffer.from(ct); bad[0] ^= 1;
try {
const d = c.createDecipheriv('aes-256-gcm', key, iv);
d.setAuthTag(tag); d.update(bad); d.final();
} catch (e) { console.log('tampered:', e.message); }
14 bytes ciphertext, 16 bytes tag
tampered: Unsupported state or unable to authenticate data
Ciphertext is as long as the plaintext, and the 16-byte tag travels with it. Flipping one bit makes decryption fail instead of returning garbage. The key and IV above are fixed only so the run is repeatable; use random bytes in real code.
const c = require('crypto');
const k = c.randomBytes(32);
console.log(k.length, 'random key bytes;', c.randomBytes(12).length, 'random nonce bytes');
try { c.createCipheriv('aes-256-gcm', Buffer.alloc(16), Buffer.alloc(12)); }
catch (e) { console.log(e.code, e.message); }
32 random key bytes; 12 random nonce bytes
ERR_CRYPTO_INVALID_KEYLEN Invalid key length
AES-256 needs exactly 32 bytes. A 16-byte key is valid for AES-128 only, so the algorithm name and key length must match.
const c = require('crypto');
const key = Buffer.alloc(32, 1), iv = Buffer.alloc(16, 3);
for (const n of [1, 15, 16, 17]) {
const x = c.createCipheriv('aes-256-cbc', key, iv);
console.log('plaintext', n, '-> ciphertext', Buffer.concat([x.update(Buffer.alloc(n)), x.final()]).length);
}
plaintext 1 -> ciphertext 16
plaintext 15 -> ciphertext 16
plaintext 16 -> ciphertext 32
plaintext 17 -> ciphertext 32
PKCS#7 padding always adds at least one byte, so an exact multiple of 16 grows by a full block.
const c = require('crypto');
const { publicKey } = c.generateKeyPairSync('rsa', { modulusLength: 2048 });
const enc = c.publicEncrypt({ key: publicKey, oaepHash: 'sha256' }, Buffer.from('attack at dawn'));
console.log('ciphertext', enc.length, 'bytes; max plaintext', 2048 / 8 - 2 * 32 - 2);
try { c.publicEncrypt({ key: publicKey, oaepHash: 'sha256' }, Buffer.alloc(191)); }
catch (e) { console.log(e.code); }
ciphertext 256 bytes; max plaintext 190
ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE
RSA-OAEP with SHA-256 on a 2048-bit key carries at most 190 bytes. For anything larger, encrypt the data with AES-GCM and encrypt only the AES key with RSA.
const c = require('crypto');
const ed = c.generateKeyPairSync('ed25519');
const sig = c.sign(null, Buffer.from('attack at dawn'), ed.privateKey);
console.log('signature', sig.length, 'bytes');
const a = c.generateKeyPairSync('x25519'), b = c.generateKeyPairSync('x25519');
const s1 = c.diffieHellman({ privateKey: a.privateKey, publicKey: b.publicKey });
const s2 = c.diffieHellman({ privateKey: b.privateKey, publicKey: a.publicKey });
console.log('shared secret', s1.length, 'bytes, equal:', s1.equals(s2));
signature 64 bytes
shared secret 32 bytes, equal: true
Both sides compute the same 32-byte secret without ever sending it. Run it through a key derivation function before using it as an AES key.
echo -n 'attack at dawn' | openssl enc -aes-256-cbc -pbkdf2 -pass pass:hunter2 \
-S 0102030405060708 -iv 000102030405060708090A0B0C0D0E0F -base64
BD8RUnPqe2H9z5JoCpXg4Q==
The -pbkdf2 flag derives the key from the password. This mode is CBC, so it has no tamper detection; the salt and IV are fixed here only to make the output repeatable.
ct1^ct2 == pt1^pt2: true. Generate a fresh random 12-byte nonce per message, or use a counter you never repeat.ecb block1==block2: true.