DEV Community

Cover image for Building Zero-Trust Privacy Utilities with Web Crypto API & Canvas (Zero Backend, 100% In-Browser)
Bhutto Sahab
Bhutto Sahab

Posted on Originally published at privacykit-kappa.vercel.app

Building Zero-Trust Privacy Utilities with Web Crypto API & Canvas (Zero Backend, 100% In-Browser)

Every developer uses online file and text utilities daily: PDF metadata scrubbers, photo EXIF strippers, AES-256 encryptors, and prompt cleaners.

However, a serious architectural flaw exists across most legacy web utilities: your sensitive files and tokens get transmitted to a remote backend server for processing. Whether it's an authorization bearer token, a confidential legal PDF, or a photo with embedded GPS coordinates, trusting a third-party server creates unnecessary security liability.

Zero-Trust Architecture Diagram: Cloud vs Browser Processing

In this guide, we'll explore how modern browser APIs—specifically the Web Cryptography API (SubtleCrypto) and HTML5 Canvas 2D API—allow us to build high-performance, military-grade security utilities that run 100% in client-side RAM with zero server infrastructure.


1. AES-256-GCM Encryption with PBKDF2 (Native Web Crypto)

Instead of pulling heavy NPM packages like CryptoJS, modern browsers ship with hardware-accelerated cryptographic primitives via window.crypto.subtle.

Here is a production-ready TypeScript implementation that derives a 256-bit key using PBKDF2 with 250,000 SHA-256 iterations and encrypts plaintext using AES-GCM (Authenticated Encryption with Associated Data):

export async function encryptClientSide(plaintext: string, passphrase: string): Promise<string> {
  const enc = new TextEncoder();
  const salt = window.crypto.getRandomValues(new Uint8Array(16));
  const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV for GCM

  // 1. Import raw passphrase into CryptoKey
  const keyMaterial = await window.crypto.subtle.importKey(
    'raw',
    enc.encode(passphrase),
    { name: 'PBKDF2' },
    false,
    ['deriveKey']
  );

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

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

  // 4. Pack Salt + IV + Ciphertext into single Base64 bundle
  const combined = new Uint8Array(salt.length + iv.length + encryptedBuffer.byteLength);
  combined.set(salt, 0);
  combined.set(iv, salt.length);
  combined.set(new Uint8Array(encryptedBuffer), salt.length + iv.length);

  return btoa(String.fromCharCode(...combined));
}
Enter fullscreen mode Exit fullscreen mode

Why AES-GCM Over AES-CBC?

AES-GCM is an AEAD cipher that includes built-in tamper detection (128-bit authentication tag). If a single bit in the ciphertext is altered or if the decryption passphrase is incorrect, crypto.subtle.decrypt() automatically throws a cryptographic exception. This eliminates the need for custom HMAC verification and protects against padding oracle vulnerabilities.


2. In-Browser Forensic EXIF Stripping via Canvas Rasterization

Removing GPS coordinates, camera serial numbers, and thumbnail previews from JPEG/PNG images can be done without server-side ImageMagick or Node.js backends.

By drawing the image onto an offscreen HTML5 <canvas> element and re-encoding it into a fresh raster blob, all ancillary EXIF, TIFF, and XMP metadata blocks are stripped cleanly in browser RAM:

export async function stripExifClientSide(imageFile: File): Promise<Blob> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const objectUrl = URL.createObjectURL(imageFile);

    img.onload = () => {
      URL.revokeObjectURL(objectUrl);
      const canvas = document.createElement('canvas');
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;

      const ctx = canvas.getContext('2d');
      if (!ctx) return reject(new Error('Canvas 2D context unavailable'));

      ctx.drawImage(img, 0, 0);
      canvas.toBlob(
        (blob) => {
          if (blob) resolve(blob);
          else reject(new Error('Blob generation failed'));
        },
        imageFile.type,
        0.95
      );
    };

    img.onerror = () => reject(new Error('Image failed to load'));
    img.src = objectUrl;
  });
}
Enter fullscreen mode Exit fullscreen mode

3. Two-Way AI Prompt Sanitization (Mask & Restore PII)

When sending internal source code, error logs, or confidential documents to LLMs like ChatGPT or Claude, Personally Identifiable Information (PII) such as emails, IP addresses, and API keys must be intercepted before transmission.

Two-Way Client-Side PII Masking and Restore Architecture

The Two-Way Masking Workflow:

  1. Interception: A client-side regex engine replaces sensitive patterns with synthetic tokens ([EMAIL_1], [IP_1], [API_KEY_1]).
  2. Local Session Map: A temporary Map<string, string> stores the mapping strictly in volatile browser memory.
  3. LLM Execution: The sanitized prompt is pasted into ChatGPT without leaking the real identities.
  4. 1-Click Restore: When ChatGPT returns the response containing the placeholder tokens, the local lookup table swaps the original values back into place.

4. Stripping URL Tracking Query Parameters

Marketing platforms and social networks append tracking parameters (utm_source, fbclid, gclid, mc_eid) to URLs to build invasive cross-site user profiles.

Anatomy of URL Tracking Parameters and Clean Link Architecture

A lightweight client-side URL sanitizer strips all invasive query parameters using the standard URL and URLSearchParams browser APIs:

const TRACKING_PARAMS = new Set([
  'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
  'fbclid', 'gclid', 'msclkid', 'dclid', 'twclid', 'mc_eid', '_ga'
]);

export function cleanUrl(rawUrl: string): string {
  try {
    const parsed = new URL(rawUrl);
    for (const key of Array.from(parsed.searchParams.keys())) {
      if (TRACKING_PARAMS.has(key.toLowerCase()) || key.startsWith('utm_')) {
        parsed.searchParams.delete(key);
      }
    }
    return parsed.toString();
  } catch {
    return rawUrl;
  }
}
Enter fullscreen mode Exit fullscreen mode

🚀 Live Implementation & Open Architecture

If you want to test these utilities live or inspect network requests in Chrome DevTools to verify that zero bytes leave your device, check out the live suite on PrivacyKit.

Key Architectural Benefits for Web Engineers:

  1. $0 Backend Infrastructure: Serving pure client-side static bundles on Vercel/Cloudflare Pages means infinite scalability with zero server compute costs.
  2. Instant Offline Execution: Utilities continue to execute without interruption even if you disconnect from Wi-Fi.
  3. GDPR by Design: No server database means zero risk of data breach liabilities under GDPR Article 5(1)(c).

What client-side tools are you currently building with the Web Crypto API? Let's discuss in the comments below! 👇

Top comments (0)