In an era where data breaches and cyber threats are increasingly common, encryption has become one of the most critical tools for protecting sensitive information. Whether you're building a web application, securing communications, or storing user data, understanding encryption fundamentals is essential for every developer.
What is Encryption?
Encryption is the process of converting plaintext data into ciphertext, an unreadable format that can only be deciphered by authorized parties with the correct decryption key. This mathematical process ensures that even if data is intercepted or accessed by unauthorized individuals, it remains meaningless without the key.
The Core Components
Plaintext: The original, readable data that you want to protect
Ciphertext: The encrypted, unreadable version of the data
Encryption Key: A string of data used in conjunction with an algorithm to encrypt and decrypt information
Encryption Algorithm: The mathematical process that transforms plaintext into ciphertext
Why Encryption Matters
Modern applications handle vast amounts of sensitive data: personal information, financial details, health records, authentication credentials, and confidential communications. Encryption provides:
- Confidentiality: Ensures data remains private
- Integrity: Verifies data hasn't been tampered with
- Authentication: Confirms the identity of communicating parties
- Non-repudiation: Prevents parties from denying their actions
Types of Encryption
Symmetric Encryption
Symmetric encryption uses the same key for both encryption and decryption. It's fast and efficient, making it ideal for encrypting large amounts of data.
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
// Generate a random key and IV
const key = randomBytes(32); // 256-bit key
const iv = randomBytes(16); // Initialization vector
// Encrypt data
function encrypt(text: string): string {
const cipher = createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
// Decrypt data
function decrypt(encryptedText: string): string {
const decipher = createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
const original = 'Sensitive data to protect';
const encrypted = encrypt(original);
const decrypted = decrypt(encrypted);
console.log('Original:', original);
console.log('Encrypted:', encrypted);
console.log('Decrypted:', decrypted);Asymmetric Encryption
Asymmetric encryption uses a pair of keys: a public key for encryption and a private key for decryption. This enables secure communication without sharing secret keys.
import { generateKeyPairSync, publicEncrypt, privateDecrypt } from 'crypto';
// Generate key pair
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
});
// Encrypt with public key
function encryptWithPublicKey(text: string): Buffer {
const buffer = Buffer.from(text, 'utf8');
return publicEncrypt(publicKey, buffer);
}
// Decrypt with private key
function decryptWithPrivateKey(encrypted: Buffer): string {
const decrypted = privateDecrypt(privateKey, encrypted);
return decrypted.toString('utf8');
}
const message = 'Secret message';
const encrypted = encryptWithPublicKey(message);
const decrypted = decryptWithPrivateKey(encrypted);
console.log('Original:', message);
console.log('Encrypted:', encrypted.toString('hex'));
console.log('Decrypted:', decrypted);Common Encryption Algorithms
AES (Advanced Encryption Standard)
AES is the most widely used symmetric encryption algorithm. It's fast, secure, and supported by virtually all platforms.
- Key Sizes: 128, 192, or 256 bits
- Block Size: 128 bits
- Use Cases: File encryption, database encryption, secure communications
RSA (Rivest-Shamir-Adleman)
RSA is the most common asymmetric encryption algorithm, based on the mathematical difficulty of factoring large prime numbers.
- Key Size: Typically 2048 or 4096 bits
- Use Cases: Key exchange, digital signatures, small data encryption
ChaCha20
A modern stream cipher that's faster than AES in software-only implementations.
- Key Size: 256 bits
- Use Cases: Mobile applications, real-time communications
Best Practices for Implementation
1. Never Roll Your Own Crypto
Always use well-established, peer-reviewed cryptographic libraries. Implementing your own encryption is virtually guaranteed to have vulnerabilities.
// ❌ BAD: Custom "encryption"
function badEncrypt(text: string, key: string): string {
return text.split('').map((c, i) =>
String.fromCharCode(c.charCodeAt(0) ^ key.charCodeAt(i % key.length))
).join('');
}
// ✅ GOOD: Use established libraries
import { createCipheriv } from 'crypto';
const cipher = createCipheriv('aes-256-gcm', key, iv);2. Use Authenticated Encryption
Always use authenticated encryption modes like AES-GCM or ChaCha20-Poly1305 that provide both confidentiality and integrity.
import { createCipheriv, createDecipheriv } from 'crypto';
function encryptWithAuth(text: string, key: Buffer, iv: Buffer) {
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get authentication tag
const authTag = cipher.getAuthTag();
return { encrypted, authTag: authTag.toString('hex') };
}3. Manage Keys Securely
Never hardcode keys in your source code or commit them to version control.
// ❌ BAD: Hardcoded key
const API_KEY = '1234567890abcdef';
// ✅ GOOD: Load from environment
const key = process.env.ENCRYPTION_KEY;
// ✅ BETTER: Use a key management service
import { SecretsManager } from 'aws-sdk';
const secret = await SecretsManager.getSecretValue('production/encryption-key');4. Use Proper Initialization Vectors
Always use a random, unique IV for each encryption operation. Never reuse IVs.
import { randomBytes } from 'crypto';
// Generate a new IV for each encryption
const iv = randomBytes(16);
// Store IV alongside ciphertext (it's not secret)
const result = {
iv: iv.toString('hex'),
ciphertext: encrypted,
};Real-World Implementation Scenarios
Encrypting Database Fields
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
class FieldEncryption {
private key: Buffer;
constructor(password: string, salt: string) {
// Derive key from password
this.key = scryptSync(password, salt, 32);
}
encryptField(value: string): { iv: string; data: string } {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', this.key, iv);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
iv: iv.toString('hex'),
data: encrypted,
};
}
decryptField(encrypted: { iv: string; data: string }): string {
const iv = Buffer.from(encrypted.iv, 'hex');
const decipher = createDecipheriv('aes-256-cbc', this.key, iv);
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage
const encryption = new FieldEncryption(
process.env.DB_ENCRYPTION_KEY!,
process.env.DB_ENCRYPTION_SALT!
);
// Encrypt before saving
const encryptedSSN = encryption.encryptField(user.ssn);
await database.save({ ...user, ssn: encryptedSSN });
// Decrypt after retrieving
const record = await database.findOne(id);
const ssn = encryption.decryptField(record.ssn);Secure API Communication
import { createSign, createVerify } from 'crypto';
// Sign outgoing requests
function signRequest(data: any, privateKey: string): string {
const sign = createSign('SHA256');
sign.update(JSON.stringify(data));
sign.end();
return sign.sign(privateKey, 'base64');
}
// Verify incoming requests
function verifyRequest(data: any, signature: string, publicKey: string): boolean {
const verify = createVerify('SHA256');
verify.update(JSON.stringify(data));
verify.end();
return verify.verify(publicKey, signature, 'base64');
}Common Pitfalls to Avoid
1. Using Weak Key Sizes
// ❌ BAD: 1024-bit RSA is deprecated
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 1024 });
// ✅ GOOD: Use at least 2048 bits
const { publicKey: goodKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });2. Reusing Keys Across Applications
Each application or context should have its own unique keys.
3. Forgetting to Encrypt Data at Rest
Many developers encrypt data in transit but forget to encrypt stored data.
4. Ignoring Key Rotation
Regularly rotate encryption keys to limit the impact of potential compromises.
Testing Your Implementation
Always test encryption implementations thoroughly:
import { describe, it, expect } from 'vitest';
describe('Encryption', () => {
it('should encrypt and decrypt correctly', () => {
const original = 'Test data';
const encrypted = encrypt(original);
const decrypted = decrypt(encrypted);
expect(decrypted).toBe(original);
expect(encrypted).not.toBe(original);
});
it('should produce different ciphertext for same plaintext', () => {
const plaintext = 'Same data';
const encrypted1 = encrypt(plaintext);
const encrypted2 = encrypt(plaintext);
expect(encrypted1).not.toBe(encrypted2);
expect(decrypt(encrypted1)).toBe(decrypt(encrypted2));
});
});Conclusion
Encryption is a fundamental tool for protecting data in modern applications. By understanding the basics, following best practices, and using established libraries, you can significantly improve the security of your applications.
Remember: encryption is just one part of a comprehensive security strategy. Combine it with other security measures like authentication, authorization, input validation, and secure coding practices to build truly secure applications.