Skip to main content

Command Palette

Search for a command to run...

How to Encrypt Sensitive Data in LocalStorage (When You Really Have No Choice)

Published
3 min read
How to Encrypt Sensitive Data in LocalStorage (When You Really Have No Choice)

In an ideal world, sensitive data would never touch localStorage. Session state would live in httpOnly cookies, large client-side data would sit in IndexedDB, and secrets would stay firmly on the server.

But production is rarely ideal.

Sometimes you are building an offline-first PWA. Sometimes you need to preserve a partially completed form so users do not lose progress on refresh. Sometimes backend constraints make proper cookie-based auth impossible. When you are forced into these trade-offs, the real danger is storing plain JSON in localStorage.

If an attacker finds an XSS vulnerability, extracting user data becomes a one-liner:

localStorage.getItem('user_data')

While client-side storage can never be made fully safe, you can raise the bar significantly by encrypting data at rest using the browser’s native Web Crypto API.

The Wrong Approach: Base64 Encoding

A common mistake looks like this:

// This is NOT encryption
const encoded = btoa(JSON.stringify(data));
localStorage.setItem('data', encoded);

Base64 is encoding, not encryption. It offers zero protection. Anyone can reverse it instantly. Treating Base64 as security is equivalent to hiding a password with a sticky note over it.

The Correct Approach: AES-GCM via Web Crypto

To do this properly, we use AES-GCM (Advanced Encryption Standard with Galois or Counter Mode). It provides confidentiality and integrity, meaning encrypted data cannot be read or modified without detection.

This solution uses the browser’s native crypto implementation. No third-party libraries, no extra dependencies, and better performance.

The Core Problem: Key Management

Encryption is useless if you store the key next to the data. Locking data with a key stored in localStorage is the digital equivalent of taping the key to the door.

The RAM Key Pattern

1. Generate the encryption key in memory when the app starts
2. Encrypt the data
3. Store only the encrypted blob in localStorage
4. When the tab closes, the key disappears

Once the page reloads or the tab closes, the encrypted data becomes unreadable without the key.

Example Implementation (Angular Service)

import { Injectable } from '@angular/core';@Injectable({ providedIn: 'root' })
export class SecureStorageService {
  private key: CryptoKey | null = null;  async init() {
    this.key = await window.crypto.subtle.generateKey(
      { name: 'AES-GCM', length: 256 },
      true,
      ['encrypt', 'decrypt']
    );
  }  async setItem(key: string, value: any): Promise<void> {
    if (!this.key) await this.init();    const encoder = new TextEncoder();
    const encoded = encoder.encode(JSON.stringify(value));
    const iv = window.crypto.getRandomValues(new Uint8Array(12));    const encrypted = await window.crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.key!,
      encoded
    );    localStorage.setItem(
      key,
      JSON.stringify({
        iv: Array.from(iv),
        content: Array.from(new Uint8Array(encrypted))
      })
    );
  }  async getItem<T>(key: string): Promise<T | null> {
    const raw = localStorage.getItem(key);
    if (!raw || !this.key) return null;    try {
      const { iv, content } = JSON.parse(raw);      const decrypted = await window.crypto.subtle.decrypt(
        { name: 'AES-GCM', iv: new Uint8Array(iv) },
        this.key,
        new Uint8Array(content)
      );      return JSON.parse(new TextDecoder().decode(decrypted));
    } catch {
      console.error('Decryption failed or data was modified');
      return null;
    }
  }
}

Security Reality Check

What This Protects Against

  • Physical device access

  • Casual inspection in DevTools

  • Disk-level data exposure

What This Does Not Protect Against….

Read my complete Blog at,
https://www.hexplain.space/blog/ZXE8mWkOSk8ZckvLXCgi