Text Encryption Guide: AES-256 Encryption with Web Crypto API

By Suvom Das March 12, 2026 10 min read

Text encryption is essential for protecting sensitive information in modern applications. This comprehensive guide covers AES-256 encryption using the Web Crypto API, best practices for secure encryption/decryption, and implementation patterns for browser-based cryptography.

Understanding Text Encryption

Text encryption transforms readable plaintext into unreadable ciphertext using cryptographic algorithms and keys. Proper encryption ensures that only authorized parties with the correct key can decrypt and read the original message.

Why Encrypt Text?

AES-256 Encryption Basics

What is AES?

AES (Advanced Encryption Standard) is a symmetric encryption algorithm adopted by the U.S. government in 2001. AES-256 uses a 256-bit key, providing extremely strong security against brute-force attacks.

Key Properties

Web Crypto API Implementation

Basic Encryption

async function encryptText(plaintext, password) {
  // Convert password to key
  const enc = new TextEncoder();
  const passwordBuffer = enc.encode(password);

  // Import password as key material
  const keyMaterial = await window.crypto.subtle.importKey(
    'raw',
    passwordBuffer,
    'PBKDF2',
    false,
    ['deriveBits', 'deriveKey']
  );

  // Generate salt
  const salt = window.crypto.getRandomValues(new Uint8Array(16));

  // Derive AES key from password
  const key = await window.crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 100000,
      hash: 'SHA-256'
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  // Generate IV
  const iv = window.crypto.getRandomValues(new Uint8Array(12));

  // Encrypt
  const plaintextBuffer = enc.encode(plaintext);
  const ciphertext = await window.crypto.subtle.encrypt(
    {
      name: 'AES-GCM',
      iv: iv
    },
    key,
    plaintextBuffer
  );

  // Combine salt + IV + ciphertext
  const result = new Uint8Array(
    salt.length + iv.length + ciphertext.byteLength
  );
  result.set(salt, 0);
  result.set(iv, salt.length);
  result.set(new Uint8Array(ciphertext), salt.length + iv.length);

  // Convert to base64
  return btoa(String.fromCharCode(...result));
}

// Usage
const encrypted = await encryptText('Secret message', 'my-password');
console.log(encrypted);

Basic Decryption

async function decryptText(encryptedData, password) {
  // Decode base64
  const data = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));

  // Extract salt, IV, and ciphertext
  const salt = data.slice(0, 16);
  const iv = data.slice(16, 28);
  const ciphertext = data.slice(28);

  // Convert password to key
  const enc = new TextEncoder();
  const passwordBuffer = enc.encode(password);

  // Import password as key material
  const keyMaterial = await window.crypto.subtle.importKey(
    'raw',
    passwordBuffer,
    'PBKDF2',
    false,
    ['deriveBits', 'deriveKey']
  );

  // Derive same AES key
  const key = await window.crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 100000,
      hash: 'SHA-256'
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  // Decrypt
  const plaintext = await window.crypto.subtle.decrypt(
    {
      name: 'AES-GCM',
      iv: iv
    },
    key,
    ciphertext
  );

  // Convert to string
  const dec = new TextDecoder();
  return dec.decode(plaintext);
}

// Usage
const decrypted = await decryptText(encrypted, 'my-password');
console.log(decrypted); // "Secret message"

Encryption Modes

AES-GCM (Recommended)

// Galois/Counter Mode - Authenticated encryption
const config = {
  name: 'AES-GCM',
  iv: iv,  // 12 bytes
  tagLength: 128  // Authentication tag length
};

// Provides:
// - Confidentiality (encryption)
// - Authenticity (detects tampering)
// - Faster than CBC + HMAC

AES-CBC

// Cipher Block Chaining - Requires HMAC for authentication
const config = {
  name: 'AES-CBC',
  iv: iv  // 16 bytes
};

// Note: Use with HMAC for authenticated encryption
// GCM is preferred for modern applications

Key Derivation with PBKDF2

Password-Based Key Derivation

async function deriveKey(password, salt, iterations = 100000) {
  const enc = new TextEncoder();
  const passwordBuffer = enc.encode(password);

  // Import password
  const keyMaterial = await window.crypto.subtle.importKey(
    'raw',
    passwordBuffer,
    'PBKDF2',
    false,
    ['deriveKey']
  );

  // Derive AES key
  return await window.crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: iterations,  // Higher = more secure but slower
      hash: 'SHA-256'
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    true,  // extractable
    ['encrypt', 'decrypt']
  );
}

// Recommended iterations:
// - 100,000+ for production
// - 600,000+ for high security
// - 1,000,000+ for maximum security (slower)

File Encryption

Encrypt Files

async function encryptFile(file, password) {
  // Read file
  const fileBuffer = await file.arrayBuffer();

  // Generate key
  const salt = window.crypto.getRandomValues(new Uint8Array(16));
  const key = await deriveKey(password, salt);

  // Generate IV
  const iv = window.crypto.getRandomValues(new Uint8Array(12));

  // Encrypt
  const ciphertext = await window.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: iv },
    key,
    fileBuffer
  );

  // Create encrypted file
  const result = new Blob([salt, iv, ciphertext], {
    type: 'application/octet-stream'
  });

  return result;
}

// Usage
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const encrypted = await encryptFile(file, 'password123');

// Download encrypted file
const url = URL.createObjectURL(encrypted);
const a = document.createElement('a');
a.href = url;
a.download = `${file.name}.encrypted`;
a.click();

Security Best Practices

1. Use Strong Passwords

// Bad passwords
'password'
'123456'
'admin'

// Good passwords (20+ characters recommended)
'correct-horse-battery-staple-2024'
'My$ecure!P@ssw0rd#2024'
'Th1s!sA$tr0ng&L0ngP@ssw0rd'

// Password strength check
function checkPasswordStrength(password) {
  const minLength = 12;
  const hasUpper = /[A-Z]/.test(password);
  const hasLower = /[a-z]/.test(password);
  const hasNumber = /\d/.test(password);
  const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);

  const strength = [
    password.length >= minLength,
    hasUpper,
    hasLower,
    hasNumber,
    hasSpecial
  ].filter(Boolean).length;

  return {
    score: strength,
    verdict: strength >= 4 ? 'Strong' :
             strength >= 3 ? 'Medium' : 'Weak'
  };
}

2. Generate Secure Random Values

// Good - Cryptographically secure
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const salt = window.crypto.getRandomValues(new Uint8Array(16));

// Bad - NOT cryptographically secure
const badIV = Math.random(); // Never use Math.random() for crypto!

3. Never Reuse IVs

// Always generate new IV for each encryption
async function encrypt(data, key) {
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  // ... encryption code
}

// Never do this:
const STATIC_IV = new Uint8Array(12); // WRONG! Security vulnerability

4. Store Salt and IV with Ciphertext

// Correct storage format:
// [salt (16 bytes)][IV (12 bytes)][ciphertext (variable)]

// Salt and IV can be public - they're not secret
// Only the key/password must remain secret

5. Handle Errors Securely

async function secureDecrypt(encrypted, password) {
  try {
    return await decryptText(encrypted, password);
  } catch (error) {
    // Don't reveal specific error details
    throw new Error('Decryption failed. Invalid password or corrupted data.');
  }
}

Common Use Cases

Secure Note Taking

async function saveEncryptedNote(content, password) {
  const encrypted = await encryptText(content, password);
  localStorage.setItem('encrypted_note', encrypted);
}

async function loadEncryptedNote(password) {
  const encrypted = localStorage.getItem('encrypted_note');
  if (!encrypted) return null;
  return await decryptText(encrypted, password);
}

API Key Storage

async function storeAPIKey(apiKey, masterPassword) {
  const encrypted = await encryptText(apiKey, masterPassword);
  localStorage.setItem('api_key_encrypted', encrypted);
}

async function retrieveAPIKey(masterPassword) {
  const encrypted = localStorage.getItem('api_key_encrypted');
  return await decryptText(encrypted, masterPassword);
}

Secure Data Transmission

async function sendEncryptedData(data, recipientPublicKey) {
  // Encrypt with shared secret
  const encrypted = await encryptText(
    JSON.stringify(data),
    sharedSecret
  );

  // Send encrypted data
  await fetch('/api/secure-endpoint', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ encrypted })
  });
}

Testing Encryption

Unit Tests

// Test encryption/decryption round trip
async function testEncryption() {
  const original = 'Test message 123!@#';
  const password = 'test-password';

  const encrypted = await encryptText(original, password);
  const decrypted = await decryptText(encrypted, password);

  console.assert(
    decrypted === original,
    'Decryption failed to match original'
  );

  // Test wrong password
  try {
    await decryptText(encrypted, 'wrong-password');
    console.assert(false, 'Should have thrown error');
  } catch (e) {
    console.log('Correctly rejected wrong password');
  }
}

testEncryption();

Performance Considerations

Optimize Large Data

// For large files, process in chunks
async function encryptLargeFile(file, password, chunkSize = 1024 * 1024) {
  const chunks = [];
  let offset = 0;

  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    const encrypted = await encryptFile(chunk, password);
    chunks.push(encrypted);
    offset += chunkSize;

    // Report progress
    console.log(`Progress: ${Math.round(offset / file.size * 100)}%`);
  }

  return new Blob(chunks);
}

Browser Compatibility

// Check Web Crypto API support
if (!window.crypto || !window.crypto.subtle) {
  alert('Your browser does not support Web Crypto API');
} else {
  // Safe to use encryption
  console.log('Web Crypto API supported');
}

// Feature detection
async function checkCryptoSupport() {
  const features = {
    subtle: !!window.crypto?.subtle,
    aesGcm: false,
    pbkdf2: false
  };

  try {
    // Test AES-GCM
    const key = await window.crypto.subtle.generateKey(
      { name: 'AES-GCM', length: 256 },
      true,
      ['encrypt', 'decrypt']
    );
    features.aesGcm = true;

    // Test PBKDF2
    const keyMaterial = await window.crypto.subtle.importKey(
      'raw',
      new Uint8Array(16),
      'PBKDF2',
      false,
      ['deriveKey']
    );
    features.pbkdf2 = true;
  } catch (e) {
    console.error('Crypto feature not supported:', e);
  }

  return features;
}

Conclusion

Text encryption using AES-256 and the Web Crypto API provides robust security for sensitive data in browser-based applications. By following best practices for key generation, IV management, and password handling, you can implement secure encryption that protects user data effectively.

Use QuickUtil.dev's Text Encryption tool to quickly encrypt and decrypt text using AES-256 with a simple interface. All encryption happens client-side, ensuring your data never leaves your browser.

Frequently Asked Questions

What is AES-256 encryption?

AES-256 (Advanced Encryption Standard with 256-bit key) is a symmetric encryption algorithm considered highly secure. It's used by governments and organizations worldwide for protecting classified and sensitive data.

How do I encrypt text in the browser?

Use the Web Crypto API's SubtleCrypto interface. Generate a key, create an initialization vector (IV), and use crypto.subtle.encrypt() with AES-GCM mode for authenticated encryption with additional data protection.

Is Web Crypto API secure?

Yes, Web Crypto API is secure and designed for cryptographic operations in browsers. It uses native implementations, keeps keys in secure memory, and prevents extraction of raw key material.

What's the difference between AES-GCM and AES-CBC?

AES-GCM provides authenticated encryption (confidentiality + integrity) and is faster. AES-CBC only provides confidentiality and requires separate authentication (HMAC). Use AES-GCM for modern applications.

Can encrypted text be decrypted without the key?

No, AES-256 encrypted data cannot be practically decrypted without the correct key. Brute-forcing 256-bit AES would take billions of years with current technology.

How long should my encryption password be?

Use at least 12-16 characters with mixed case, numbers, and symbols. Longer passwords (20+ characters) are better. Use passphrases for memorability while maintaining security.

What is an initialization vector (IV)?

An IV is a random value used with encryption algorithms to ensure identical plaintexts encrypt to different ciphertexts. It must be unique for each encryption operation but doesn't need to be secret.

Should I encrypt data client-side or server-side?

Client-side encryption protects data before transmission, ensuring server never sees plaintext. Server-side handles key management better. Use client-side for end-to-end encryption, server-side for data at rest.

Encrypt Your Text Securely

Protect sensitive information with AES-256 encryption. All encryption happens in your browser - your data never leaves your device.

Try the Text Encryption Tool Now