Encryption: Symmetric, Asymmetric, TLS, and Key Management
Encryption is the foundation of data security. It transforms readable data into an unreadable format that can only be reversed with the correct key. From protecting passwords with hashing to securing API traffic with TLS, encryption touches every layer of a modern system. This guide covers the essential encryption concepts every engineer needs, with practical code examples and guidance for system design interviews.
Symmetric vs Asymmetric Encryption
| Feature | Symmetric | Asymmetric |
|---|---|---|
| Keys | One shared secret key | Public key + private key pair |
| Speed | Very fast (1000x faster) | Slow (computationally expensive) |
| Key Distribution | Challenging (shared secret) | Easy (public key is public) |
| Data Size | Any size (stream or block) | Limited (usually encrypts keys only) |
| Algorithms | AES, ChaCha20 | RSA, ECDSA, Ed25519 |
| Use Case | Bulk data encryption | Key exchange, digital signatures |
In practice, TLS uses both: asymmetric encryption for the handshake (key exchange) and symmetric encryption for the data transfer. This hybrid approach gives you the key distribution benefits of asymmetric with the speed of symmetric.
AES (Advanced Encryption Standard)
AES is the gold standard for symmetric encryption, used by governments and enterprises worldwide. It operates on fixed-size blocks (128 bits) with key sizes of 128, 192, or 256 bits.
AES Modes Comparison
| Mode | Authentication | Parallelizable | Recommendation |
|---|---|---|---|
| ECB | No | Yes | Never use — identical blocks produce identical ciphertext |
| CBC | No | Decrypt only | Legacy — vulnerable to padding oracle attacks |
| CTR | No | Yes | Good for streaming, but no integrity check |
| GCM | Yes (AEAD) | Yes | Recommended — encryption + authentication |
AES-256-GCM Implementation
const crypto = require('crypto');
function encrypt(plaintext, key) {
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let ciphertext = cipher.update(plaintext, 'utf8', 'hex');
ciphertext += cipher.final('hex');
const authTag = cipher.getAuthTag(); // 128-bit authentication tag
// Return IV + authTag + ciphertext (all needed for decryption)
return {
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
ciphertext: ciphertext
};
}
function decrypt(encrypted, key) {
const iv = Buffer.from(encrypted.iv, 'hex');
const authTag = Buffer.from(encrypted.authTag, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let plaintext = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
plaintext += decipher.final('utf8'); // Throws if tampered
return plaintext;
}
// Generate a secure 256-bit key
const key = crypto.randomBytes(32);
const encrypted = encrypt('sensitive data', key);
const decrypted = decrypt(encrypted, key);
console.log(decrypted); // "sensitive data"
RSA (Rivest-Shamir-Adleman)
RSA is the most widely used asymmetric algorithm. It is used for key exchange, digital signatures, and encrypting small amounts of data (like symmetric keys).
const crypto = require('crypto');
// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // Minimum 2048 bits; prefer 4096 for long-term security
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// Encrypt with public key
function rsaEncrypt(plaintext, pubKey) {
return crypto.publicEncrypt(
{ key: pubKey, padding: crypto.constants.RSA_OAEP_PADDING },
Buffer.from(plaintext)
).toString('base64');
}
// Decrypt with private key
function rsaDecrypt(ciphertext, privKey) {
return crypto.privateDecrypt(
{ key: privKey, padding: crypto.constants.RSA_OAEP_PADDING },
Buffer.from(ciphertext, 'base64')
).toString('utf8');
}
// RSA can only encrypt data smaller than key size minus padding
// For larger data: encrypt with AES, encrypt AES key with RSA (hybrid)
TLS 1.3 Handshake
TLS 1.3 secures all HTTPS traffic. It is faster than TLS 1.2 (1-RTT handshake vs 2-RTT) and removes insecure cipher suites. Understanding TLS is important for API security and zero trust architectures.
TLS 1.3 Handshake Steps
- Client Hello — Client sends supported cipher suites and key share (ECDHE public key)
- Server Hello — Server selects cipher suite, sends its key share and certificate
- Finished — Both sides derive the shared secret and begin encrypted communication
# Supported TLS 1.3 cipher suites
TLS_AES_256_GCM_SHA384 # AES-256 encryption, GCM mode, SHA-384 hash
TLS_AES_128_GCM_SHA256 # AES-128 encryption, GCM mode, SHA-256 hash
TLS_CHACHA20_POLY1305_SHA256 # ChaCha20 stream cipher with Poly1305 MAC
# NGINX TLS 1.3 configuration
server {
listen 443 ssl http2;
ssl_protocols TLSv1.3;
ssl_ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=63072000" always;
}
Encryption at Rest vs In Transit
| Aspect | At Rest | In Transit |
|---|---|---|
| What it protects | Stored data (databases, files, backups) | Data moving between systems |
| Threat model | Physical theft, unauthorized disk access | Eavesdropping, man-in-the-middle |
| Technology | AES-256 (disk/volume/field level) | TLS 1.3, mTLS, VPN, IPSec |
| Examples | AWS EBS encryption, database TDE | HTTPS, encrypted database connections |
| Key management | KMS (AWS KMS, Azure Key Vault) | Certificate authorities, ACME |
Key Management
Key management is often harder than encryption itself. The security of your encrypted data depends entirely on protecting your keys.
Key Management Best Practices
- Never hardcode keys — Use environment variables, KMS, or secrets managers
- Implement key rotation — Rotate keys regularly (90 days is common)
- Use envelope encryption — Encrypt data with a data key, encrypt the data key with a master key in KMS
- Separate key management from data — Keys and encrypted data should not be stored together
- Use HSMs for high-security scenarios — Hardware Security Modules provide tamper-resistant key storage
// AWS KMS envelope encryption pattern
const AWS = require('aws-sdk');
const kms = new AWS.KMS();
async function encryptWithEnvelope(plaintext) {
// Step 1: Generate a data key from KMS
const { Plaintext: dataKey, CiphertextBlob: encryptedDataKey } =
await kms.generateDataKey({
KeyId: 'alias/my-master-key',
KeySpec: 'AES_256'
}).promise();
// Step 2: Encrypt data locally with the plaintext data key
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', dataKey, iv);
let ciphertext = cipher.update(plaintext, 'utf8', 'hex');
ciphertext += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Step 3: Discard plaintext data key from memory
dataKey.fill(0);
// Step 4: Store encrypted data key alongside ciphertext
return {
encryptedDataKey: encryptedDataKey.toString('base64'),
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
ciphertext
};
}
Hashing vs Encryption
| Feature | Hashing | Encryption |
|---|---|---|
| Reversible | No (one-way function) | Yes (with the correct key) |
| Purpose | Verify integrity, store passwords | Protect confidentiality |
| Output size | Fixed (SHA-256 always 256 bits) | Proportional to input size |
| Key required | No (but use salt for passwords) | Yes |
| Algorithms | SHA-256, bcrypt, Argon2 | AES-GCM, RSA, ChaCha20 |
Password Hashing
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
// bcrypt — battle-tested, widely supported
async function hashWithBcrypt(password) {
const salt = await bcrypt.genSalt(12); // Work factor of 12
return bcrypt.hash(password, salt);
}
// Argon2id — recommended by OWASP, memory-hard
async function hashWithArgon2(password) {
return argon2.hash(password, {
type: argon2.argon2id, // Hybrid: resistant to side-channel + GPU attacks
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4 // 4 threads
});
}
// SHA-256 — for data integrity, NOT for passwords
function sha256(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}
Try different hashing algorithms with our Security and Crypto Tools and learn about how authentication uses these hashing functions.
Digital Signatures
Digital signatures use asymmetric cryptography to prove authenticity and integrity. The sender signs with their private key, and anyone can verify with the public key. This is how JWTs are verified.
// Sign a message
function sign(message, privateKey) {
const signer = crypto.createSign('SHA256');
signer.update(message);
return signer.sign(privateKey, 'base64');
}
// Verify a signature
function verify(message, signature, publicKey) {
const verifier = crypto.createVerify('SHA256');
verifier.update(message);
return verifier.verify(publicKey, signature, 'base64');
}
const message = 'Transfer $500 to Account XYZ';
const signature = sign(message, privateKey);
const isValid = verify(message, signature, publicKey);
// isValid === true: message is authentic and untampered
Frequently Asked Questions
When should I use AES-128 vs AES-256?
Both are considered secure for the foreseeable future. AES-256 provides a larger security margin and is required by some compliance standards (FIPS 140-2 Level 3, certain government classifications). AES-128 is faster and sufficient for most applications. If in doubt, use AES-256 — the performance difference is negligible for most workloads.
Is SHA-256 safe for hashing passwords?
No. SHA-256 is a fast hash designed for data integrity. Attackers can compute billions of SHA-256 hashes per second using GPUs. For passwords, use intentionally slow hashes: bcrypt (work factor 12+), Argon2id (recommended by OWASP), or scrypt. These are designed to be computationally expensive, making brute force impractical.
What is the difference between TLS 1.2 and TLS 1.3?
TLS 1.3 is faster (1-RTT handshake vs 2-RTT), more secure (removed insecure cipher suites like RC4, 3DES, SHA-1), and simpler (fewer configurable options means fewer misconfigurations). TLS 1.3 mandates perfect forward secrecy (ECDHE), meaning past sessions cannot be decrypted even if the server key is compromised. Always prefer TLS 1.3. Visit our API and Network Tools to test your TLS configuration.
How often should encryption keys be rotated?
Key rotation frequency depends on the sensitivity: 90 days for general-purpose data keys, 365 days for master keys in KMS, immediately if a key may be compromised. Use envelope encryption so that rotating the master key does not require re-encrypting all data. AWS KMS supports automatic annual key rotation. For JWT signing keys, rotate at least annually and support multiple active keys during transition. See our tools page for more resources.