Skip to content
All essays
ArchitectureMarch 27, 202415 min

Symmetric Encryption: Fast and Efficient Data Protection

Master symmetric encryption algorithms and learn when and how to use them effectively in your applications

Ü
Ümit Uz
Mobile & Full Stack Developer

Symmetric encryption is the workhorse of modern cryptography, providing fast and efficient encryption for everything from securing database fields to encrypting entire filesystems. In this guide, we'll dive deep into symmetric encryption, explore popular algorithms, and learn best practices for implementation.

Understanding Symmetric Encryption

Symmetric encryption uses the same cryptographic key for both encryption and decryption. This shared key must be kept secret between the communicating parties, which is why it's sometimes called "secret key" or "private key" encryption.

How It Works

  1. 1Key Generation: A secret key is generated or derived
  2. 2Encryption: The plaintext is combined with the key using an encryption algorithm
  3. 3Decryption: The ciphertext is combined with the same key to recover the plaintext
typescript
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

// Shared secret (must be kept private!)
const key = randomBytes(32); // 256-bit key for AES-256

function encryptSymmetric(plaintext: string, key: Buffer): { iv: string; ciphertext: string } {
  const iv = randomBytes(16); // Unique IV for each encryption
  const cipher = createCipheriv('aes-256-gcm', key, iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return {
    iv: iv.toString('hex'),
    ciphertext: encrypted,
    authTag: authTag.toString('hex'),
  };
}

function decryptSymmetric(encrypted: { iv: string; ciphertext: string; authTag: string }, key: Buffer): string {
  const iv = Buffer.from(encrypted.iv, 'hex');
  const decipher = createDecipheriv('aes-256-gcm', key, iv);

  decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));

  let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

AES (Advanced Encryption Standard)

AES is the gold standard for symmetric encryption. It's fast, secure, and widely supported.

Key Sizes:

  • AES-128: 128-bit key (faster, slightly less secure)
  • AES-192: 192-bit key (balance between speed and security)
  • AES-256: 256-bit key (most secure, recommended for sensitive data)

Modes of Operation:

  • GCM (Galois/Counter Mode): Recommended - provides authentication
  • CBC (Cipher Block Chaining): Common but requires separate authentication
  • CTR (Counter Mode): Useful for parallel encryption
  • ECB (Electronic Codebook): ❌ Never use - insecure
typescript
import { createCipheriv, randomBytes } from 'crypto';

// AES-256-GCM (Recommended)
function encryptAES256GCM(plaintext: string, key: Buffer) {
  const iv = randomBytes(12); // GCM recommends 12-byte IV
  const cipher = createCipheriv('aes-256-gcm', key, iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return {
    iv: iv.toString('hex'),
    ciphertext: encrypted,
    authTag: authTag.toString('hex'),
  };
}

ChaCha20

ChaCha20 is a modern stream cipher that's faster than AES in software-only implementations.

Advantages:

  • Excellent performance on mobile and embedded devices
  • Resistant to timing attacks
  • No hardware acceleration needed
typescript
import { createCipheriv } from 'crypto';

function encryptChaCha20(plaintext: string, key: Buffer) {
  const nonce = randomBytes(12); // ChaCha20 uses a nonce
  const cipher = createCipheriv('chacha20-poly1305', key, nonce);

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return {
    nonce: nonce.toString('hex'),
    ciphertext: encrypted,
    authTag: authTag.toString('hex'),
  };
}

Encryption Modes Explained

ECB Mode (Electronic Codebook) - ❌ DON'T USE

ECB encrypts each block independently, which reveals patterns in the data.

typescript
// ❌ NEVER USE ECB - It's insecure!
const cipher = createCipheriv('aes-256-ecb', key, Buffer.alloc(0));

CBC Mode (Cipher Block Chaining)

CBC is better than ECB but requires proper implementation and authentication.

typescript
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

function encryptCBC(plaintext: string, key: Buffer) {
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-cbc', key, iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  return { iv: iv.toString('hex'), ciphertext: encrypted };
}

function decryptCBC(encrypted: { iv: string; ciphertext: string }, key: Buffer) {
  const iv = Buffer.from(encrypted.iv, 'hex');
  const decipher = createDecipheriv('aes-256-cbc', key, iv);

  let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

GCM provides both encryption and authentication, making it the preferred choice.

typescript
import { createCipheriv, createDecipheriv } from 'crypto';

function encryptGCM(plaintext: string, key: Buffer, associatedData?: string) {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);

  // Add associated data (authenticated but not encrypted)
  if (associatedData) {
    cipher.setAAD(Buffer.from(associatedData));
  }

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return {
    iv: iv.toString('hex'),
    ciphertext: encrypted,
    authTag: authTag.toString('hex'),
  };
}

function decryptGCM(encrypted: any, key: Buffer, associatedData?: string) {
  const iv = Buffer.from(encrypted.iv, 'hex');
  const decipher = createDecipheriv('aes-256-gcm', key, iv);

  decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));

  if (associatedData) {
    decipher.setAAD(Buffer.from(associatedData));
  }

  let decrypted = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

Key Management

Generating Strong Keys

typescript
import { randomBytes, scryptSync } from 'crypto';

// Method 1: Generate random key
const key = randomBytes(32); // 256-bit key

// Method 2: Derive key from password
function deriveKeyFromPassword(password: string, salt: Buffer): Buffer {
  return scryptSync(password, salt, {
    N: 16384,  // CPU/memory cost parameter
    r: 8,      // Block size parameter
    p: 1,      // Parallelization parameter
    maxmem: 32 * 1024 * 1024,
  });
}

const password = 'strong-password-here';
const salt = randomBytes(16);
const derivedKey = deriveKeyFromPassword(password, salt);

Storing Keys Securely

typescript
import { config } from 'dotenv';

// Load from environment variables
config();

const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');

// For production, use a key management service
// AWS KMS, Azure Key Vault, or Google Cloud KMS

Practical Implementation Patterns

Encrypting User Data

typescript
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

class UserDataEncryption {
  private key: Buffer;

  constructor(keyBase64: string) {
    this.key = Buffer.from(keyBase64, 'base64');
  }

  encryptPersonalInfo(data: Record<string, string>): string {
    const plaintext = JSON.stringify(data);
    const iv = randomBytes(12);
    const cipher = createCipheriv('aes-256-gcm', this.key, iv);

    let encrypted = cipher.update(plaintext, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    const authTag = cipher.getAuthTag();

    return JSON.stringify({
      iv: iv.toString('hex'),
      ciphertext: encrypted,
      authTag: authTag.toString('hex'),
    });
  }

  decryptPersonalInfo(encryptedData: string): Record<string, string> {
    const { iv, ciphertext, authTag } = JSON.parse(encryptedData);

    const decipher = createDecipheriv('aes-256-gcm', this.key, Buffer.from(iv, 'hex'));
    decipher.setAuthTag(Buffer.from(authTag, 'hex'));

    let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return JSON.parse(decrypted);
  }
}

// Usage
const encryption = new UserDataEncryption(process.env.ENCRYPTION_KEY!);

// Encrypt before storing
const encryptedSSN = encryption.encryptPersonalInfo({
  ssn: '123-45-6789',
  dob: '1990-01-01',
});

await database.saveUser({ ssn: encryptedSSN });

// Decrypt after retrieving
const user = await database.getUser(userId);
const decrypted = encryption.decryptPersonalInfo(user.ssn);

Encrypting Files

typescript
import { createCipheriv, createDecipheriv, randomBytes, createReadStream, createWriteStream } from 'crypto';
import { promisify } from 'util';
import { pipeline } from 'stream';

const pipelineAsync = promisify(pipeline);

async function encryptFile(inputPath: string, outputPath: string, key: Buffer) {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);

  const input = createReadStream(inputPath);
  const output = createWriteStream(outputPath);

  // Write IV at the beginning
  output.write(iv);

  await pipelineAsync(input, cipher, output);

  // Get auth tag and append to file
  const authTag = cipher.getAuthTag();
  const fd = await promisify(output.open.bind(output))(outputPath, 'a');
  await promisify(output.write.bind(output))(fd, authTag);
  await promisify(output.close.bind(output))(fd);
}

async function decryptFile(inputPath: string, outputPath: string, key: Buffer) {
  const input = createReadStream(inputPath);
  const output = createWriteStream(outputPath);

  // Read IV from beginning
  const iv = await readIV(inputPath);
  const decipher = createDecipheriv('aes-256-gcm', key, iv);

  // Read auth tag from end
  const authTag = await readAuthTag(inputPath);
  decipher.setAuthTag(authTag);

  await pipelineAsync(input, decipher, output);
}

Database Encryption Helper

typescript
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

class DatabaseEncryption {
  private key: Buffer;

  constructor(keyBase64: string) {
    this.key = Buffer.from(keyBase64, 'base64');
  }

  encrypt(value: string): string {
    const iv = randomBytes(12);
    const cipher = createCipheriv('aes-256-gcm', this.key, iv);

    let encrypted = cipher.update(value, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    const authTag = cipher.getAuthTag();

    return `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`;
  }

  decrypt(value: string): string {
    const [ivHex, ciphertext, authTagHex] = value.split(':');

    const decipher = createDecipheriv('aes-256-gcm', this.key, Buffer.from(ivHex, 'hex'));
    decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));

    let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}

// Usage in database queries
const encryption = new DatabaseEncryption(process.env.DB_KEY!);

// Encrypt before saving
const encryptedEmail = encryption.encrypt('user@example.com');

// Decrypt after retrieving
const email = encryption.decrypt(row.encrypted_email);

Performance Optimization

Use Web Crypto API in Browser

typescript
// Browser-compatible encryption
async function encryptInBrowser(data: string, key: CryptoKey): Promise<string> {
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    new TextEncoder().encode(data)
  );

  // Combine IV and ciphertext
  const combined = new Uint8Array(iv.length + ciphertext.byteLength);
  combined.set(iv);
  combined.set(new Uint8Array(ciphertext), iv.length);

  return btoa(String.fromCharCode(...combined));
}

Parallel Encryption with Worker Threads

typescript
import { Worker } from 'worker_threads';
import { randomBytes } from 'crypto';

async function encryptParallel(data: string[], key: Buffer): Promise<string[]> {
  const workers = data.map((item) => {
    return new Promise<string>((resolve) => {
      const worker = new Worker('./encrypt-worker.js', {
        workerData: { data: item, key: key.toString('hex') },
      });

      worker.on('message', resolve);
    });
  });

  return Promise.all(workers);
}

Common Mistakes to Avoid

1. Reusing Initialization Vectors

typescript
// ❌ BAD: Reusing IV
const iv = Buffer.alloc(12, 0);

// ✅ GOOD: Generate new IV each time
const iv = randomBytes(12);

2. Using Weak Keys

typescript
// ❌ BAD: Short or predictable key
const key = Buffer.from('password123');

// ✅ GOOD: Strong random key
const key = randomBytes(32);

3. Forgetting Authentication

typescript
// ❌ BAD: Using CBC without HMAC
const cipher = createCipheriv('aes-256-cbc', key, iv);

// ✅ GOOD: Use authenticated encryption
const cipher = createCipheriv('aes-256-gcm', key, iv);

4. Storing Keys with Data

Never store encryption keys alongside encrypted data.

typescript
// ❌ BAD
const encrypted = { key, data: encrypt(data, key) };

// ✅ GOOD: Store key separately
const encrypted = { data: encrypt(data, key) };
// Key stored in secure key management system

Testing Your Implementation

typescript
import { describe, it, expect } from 'vitest';

describe('Symmetric Encryption', () => {
  it('should produce unique ciphertext for same plaintext', () => {
    const key = randomBytes(32);
    const plaintext = 'test data';

    const encrypted1 = encryptGCM(plaintext, key);
    const encrypted2 = encryptGCM(plaintext, key);

    expect(encrypted1.ciphertext).not.toBe(encrypted2.ciphertext);
  });

  it('should fail with wrong auth tag', () => {
    const key = randomBytes(32);
    const plaintext = 'test data';

    const encrypted = encryptGCM(plaintext, key);
    encrypted.authTag = encrypted.authTag.replace('a', 'b');

    expect(() => decryptGCM(encrypted, key)).toThrow();
  });
});

Conclusion

Symmetric encryption is fast, efficient, and essential for protecting data at rest and in transit. By following best practices—using authenticated encryption modes like AES-GCM, properly managing keys, and avoiding common pitfalls—you can build secure systems that protect sensitive data effectively.

Remember: symmetric encryption is just one piece of the security puzzle. Combine it with proper key management, access controls, and other security measures for comprehensive protection.

Enjoyed this essay? Share it.

Related essays

Next essay
Asymmetric Encryption: Public Key Cryptography Deep Dive