[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Implement End-to-End Encryption for User Data

Implement End-to-End Encryption for User Data

Add client-side encryption to a health-tech SaaS so that even a database breach exposes zero readable patient data — meeting HIPAA requirements without sacrificing search or collaboration.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 97/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

nextjs

v1.1.0

Assists with building production-grade React applications using Next.js 16. Use when working with the App Router, Server Components, Server Actions, the proxy file (formerly middleware), Cache Components, or deploying to Vercel or self-hosted environments. Trigger words: nextjs, next.js, app router, server components, server actions, use cache, proxy.ts, react framework, ssr, isr.

100/100 quality
5.29× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
>

vitest

v1.1.0

Vitest is a test runner for JavaScript and TypeScript built on Vite, with a Jest-compatible API, built-in mocking, snapshots and code coverage. Use when a user asks to set up or run unit tests in a Vite or Node project, write tests with describe/it/expect, mock modules with vi.mock, add coverage thresholds, configure vitest.config.ts or test projects, migrate from Jest, or upgrade to Vitest 5.

100/100 quality
1.50× impact
SAFE· stale score
View skill
$

The Problem

Suki is CTO of a health-tech startup that stores patient notes, lab results, and treatment plans. Their current setup encrypts data at rest (PostgreSQL disk encryption) and in transit (TLS), but a database admin or attacker with DB access can read everything in plaintext. Their HIPAA security officer flagged this as a critical gap: if the database is breached, all 50,000 patient records are exposed. A competitor had exactly this scenario last year — $4.3M fine, class-action lawsuit, and they lost 60% of their customers.

Suki needs:

  • Client-side encryption — data is encrypted before it leaves the browser, server never sees plaintext
  • Key hierarchy — per-user master keys, per-record data keys, key rotation without re-encrypting everything
  • Searchable encrypted data — doctors must find patients by name without decrypting every record
  • Sharing — a doctor shares a patient record with a specialist, who can decrypt it with their own key
  • Key recovery — if a user loses their device, they can recover via a secure process
  • Zero knowledge — the server cannot decrypt any patient data, even under subpoena for server-side data

Step 1: Key Hierarchy Design

Three-level key hierarchy: master key → key encryption key → data encryption key. This allows key rotation and sharing without re-encrypting data.

typescript
// src/crypto/key-hierarchy.ts
// Three-level key hierarchy for E2EE with sharing support

// Level 1: Master Key (derived from user's password, never leaves device)
// Level 2: Key Encryption Key (KEK) — encrypts data keys, stored encrypted on server
// Level 3: Data Encryption Key (DEK) — encrypts actual records, one per record

export interface EncryptedKey {
  ciphertext: ArrayBuffer;
  iv: Uint8Array;           // 12 bytes for AES-GCM
  salt?: Uint8Array;        // 16 bytes, only for password-derived keys
}

export interface KeyPair {
  publicKey: CryptoKey;     // for sharing — others encrypt DEKs to you
  privateKey: CryptoKey;    // for receiving — you decrypt shared DEKs
}

// Derive master key from password using PBKDF2
// 600,000 iterations = OWASP 2024 recommendation for PBKDF2-SHA256
export async function deriveMasterKey(
  password: string,
  salt: Uint8Array
): Promise<CryptoKey> {
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );

  return crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt,
      iterations: 600_000,  // OWASP 2024 minimum for SHA-256
      hash: 'SHA-256',
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,         // not extractable — stays in WebCrypto
    ['encrypt', 'decrypt']
  );
}

// Generate a random AES-256-GCM key for encrypting data
export async function generateDEK(): Promise<CryptoKey> {
  return crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,          // extractable — we need to wrap it for storage
    ['encrypt', 'decrypt']
  );
}

// Generate RSA key pair for sharing
export async function generateKeyPair(): Promise<KeyPair> {
  const pair = await crypto.subtle.generateKey(
    {
      name: 'RSA-OAEP',
      modulusLength: 4096,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-256',
    },
    true,
    ['encrypt', 'decrypt']  // public encrypts, private decrypts
  );

  return { publicKey: pair.publicKey, privateKey: pair.privateKey };
}

// Wrap (encrypt) a DEK with the user's master key for storage
export async function wrapKey(
  dek: CryptoKey,
  masterKey: CryptoKey
): Promise<EncryptedKey> {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.wrapKey(
    'raw', dek, masterKey, { name: 'AES-GCM', iv }
  );
  return { ciphertext, iv };
}

// Unwrap (decrypt) a DEK with the user's master key
export async function unwrapKey(
  encrypted: EncryptedKey,
  masterKey: CryptoKey
): Promise<CryptoKey> {
  return crypto.subtle.unwrapKey(
    'raw',
    encrypted.ciphertext,
    masterKey,
    { name: 'AES-GCM', iv: encrypted.iv },
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
}

Step 2: Encrypt and Decrypt Records

Every patient record gets its own DEK. The encrypted record and wrapped DEK are stored on the server — the server never sees the plaintext or the unwrapped key.

typescript
// src/crypto/record-crypto.ts
// Encrypts/decrypts individual records with per-record keys

import { generateDEK, wrapKey, unwrapKey, type EncryptedKey } from './key-hierarchy';

interface EncryptedRecord {
  ciphertext: string;       // base64
  iv: string;               // base64
  wrappedDek: EncryptedKey; // DEK encrypted with user's master key
  searchTokens: string[];   // blind index tokens for searchable encryption
}

export async function encryptRecord(
  plaintext: Record<string, unknown>,
  masterKey: CryptoKey,
  searchableFields: string[]  // field names to generate search tokens for
): Promise<EncryptedRecord> {
  // Generate a fresh DEK for this record
  const dek = await generateDEK();

  // Encrypt the record
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(JSON.stringify(plaintext));
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    dek,
    encoded
  );

  // Wrap the DEK with master key for storage
  const wrappedDek = await wrapKey(dek, masterKey);

  // Generate blind index tokens for searchable fields
  const searchTokens = await Promise.all(
    searchableFields.map(async (field) => {
      const value = String(plaintext[field] ?? '').toLowerCase().trim();
      if (!value) return '';
      return generateBlindIndex(value, masterKey);
    })
  );

  return {
    ciphertext: arrayBufferToBase64(ciphertext),
    iv: arrayBufferToBase64(iv),
    wrappedDek,
    searchTokens: searchTokens.filter(Boolean),
  };
}

export async function decryptRecord(
  encrypted: EncryptedRecord,
  masterKey: CryptoKey
): Promise<Record<string, unknown>> {
  // Unwrap the DEK
  const dek = await unwrapKey(encrypted.wrappedDek, masterKey);

  // Decrypt the record
  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: base64ToArrayBuffer(encrypted.iv) },
    dek,
    base64ToArrayBuffer(encrypted.ciphertext)
  );

  return JSON.parse(new TextDecoder().decode(plaintext));
}

// Blind index: HMAC of the value — server can match tokens without knowing values
async function generateBlindIndex(
  value: string,
  key: CryptoKey
): Promise<string> {
  // Derive a separate HMAC key from the master key
  const rawKey = await crypto.subtle.exportKey('raw', key);
  const hmacKey = await crypto.subtle.importKey(
    'raw', rawKey, { name: 'HMAC', hash: 'SHA-256' },
    false, ['sign']
  );

  const signature = await crypto.subtle.sign(
    'HMAC', hmacKey,
    new TextEncoder().encode(value)
  );

  // Truncate to 16 bytes — enough for collision resistance, small for indexing
  return arrayBufferToBase64(signature.slice(0, 16));
}

function arrayBufferToBase64(buf: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(buf)));
}

function base64ToArrayBuffer(b64: string): ArrayBuffer {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes.buffer;
}

Step 3: Searchable Encryption via Blind Indexes

Doctors need to search for patients by name. Blind indexes allow the server to match search queries without decrypting records.

typescript
// src/api/search.ts
// Server-side search on blind indexes — no plaintext ever touched

import { Pool } from 'pg';

const db = new Pool({ connectionString: process.env.DATABASE_URL });

// Client sends blind index token for search term
// Server matches against stored tokens — never sees the actual name
export async function searchRecords(
  userId: string,
  searchToken: string
): Promise<Array<{ id: string; ciphertext: string; iv: string; wrappedDek: any }>> {
  const result = await db.query(`
    SELECT id, ciphertext, iv, wrapped_dek
    FROM patient_records
    WHERE owner_id = $1
      AND $2 = ANY(search_tokens)
    ORDER BY created_at DESC
    LIMIT 50
  `, [userId, searchToken]);

  return result.rows.map((row) => ({
    id: row.id,
    ciphertext: row.ciphertext,
    iv: row.iv,
    wrappedDek: row.wrapped_dek,
  }));
}
typescript
// src/hooks/use-search.ts
// Client-side search — generates blind index, sends to server, decrypts results

import { generateBlindIndex } from '@/crypto/record-crypto';
import { decryptRecord } from '@/crypto/record-crypto';
import { useMasterKey } from '@/crypto/key-context';

export function useEncryptedSearch() {
  const masterKey = useMasterKey();

  async function search(query: string): Promise<Record<string, unknown>[]> {
    // Generate the same blind index the record was stored with
    const token = await generateBlindIndex(
      query.toLowerCase().trim(),
      masterKey
    );

    // Server matches the token — never sees the search term
    const response = await fetch(`/api/records/search?token=${token}`);
    const encrypted = await response.json();

    // Decrypt all matching records client-side
    return Promise.all(
      encrypted.map((rec: any) => decryptRecord(rec, masterKey))
    );
  }

  return { search };
}

Step 4: Record Sharing Between Users

When a doctor shares a record with a specialist, the DEK is re-wrapped with the specialist's public key.

typescript
// src/crypto/sharing.ts
// Share encrypted records by re-wrapping DEK with recipient's public key

import { unwrapKey, type EncryptedKey } from './key-hierarchy';

export async function shareRecord(
  wrappedDek: EncryptedKey,
  senderMasterKey: CryptoKey,
  recipientPublicKey: CryptoKey
): Promise<EncryptedKey> {
  // Unwrap DEK with sender's master key
  const dek = await unwrapKey(wrappedDek, senderMasterKey);

  // Export DEK as raw bytes
  const dekRaw = await crypto.subtle.exportKey('raw', dek);

  // Encrypt DEK with recipient's RSA public key
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'RSA-OAEP' },
    recipientPublicKey,
    dekRaw
  );

  return {
    ciphertext,
    iv: new Uint8Array(0),  // RSA-OAEP doesn't use IV
  };
}

export async function receiveSharedRecord(
  sharedDek: EncryptedKey,
  recipientPrivateKey: CryptoKey
): Promise<CryptoKey> {
  // Decrypt DEK with recipient's RSA private key
  const dekRaw = await crypto.subtle.decrypt(
    { name: 'RSA-OAEP' },
    recipientPrivateKey,
    sharedDek.ciphertext
  );

  // Import as AES key
  return crypto.subtle.importKey(
    'raw', dekRaw,
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
}

Step 5: Key Recovery via Security Questions

If a user loses their device, they recover access via security questions that derive the same master key.

typescript
// src/crypto/recovery.ts
// Key recovery using deterministic derivation from security answers

export async function setupRecovery(
  masterKey: CryptoKey,
  securityAnswers: string[]  // 3 answers, order matters
): Promise<{ recoveryBlob: string; salt: string }> {
  // Derive a recovery key from the concatenated answers
  const combined = securityAnswers
    .map((a) => a.toLowerCase().trim())
    .join('|');

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

  const recoveryKey = await deriveMasterKeyFromString(combined, salt);

  // Wrap the actual master key with the recovery key
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const exported = await crypto.subtle.exportKey('raw', masterKey);
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    recoveryKey,
    exported
  );

  // Store encrypted master key + IV on server (server can't decrypt without answers)
  const blob = JSON.stringify({
    ciphertext: arrayBufferToBase64(ciphertext),
    iv: arrayBufferToBase64(iv),
  });

  return {
    recoveryBlob: blob,
    salt: arrayBufferToBase64(salt),
  };
}

export async function recoverMasterKey(
  securityAnswers: string[],
  recoveryBlob: string,
  salt: string
): Promise<CryptoKey> {
  const combined = securityAnswers
    .map((a) => a.toLowerCase().trim())
    .join('|');

  const recoveryKey = await deriveMasterKeyFromString(
    combined,
    base64ToArrayBuffer(salt) as unknown as Uint8Array
  );

  const { ciphertext, iv } = JSON.parse(recoveryBlob);

  const rawMasterKey = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: base64ToArrayBuffer(iv) },
    recoveryKey,
    base64ToArrayBuffer(ciphertext)
  );

  return crypto.subtle.importKey(
    'raw', rawMasterKey,
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
}

async function deriveMasterKeyFromString(
  input: string, salt: Uint8Array
): Promise<CryptoKey> {
  const keyMaterial = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(input),
    'PBKDF2', false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600_000, hash: 'SHA-256' },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
}

function arrayBufferToBase64(buf: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(buf)));
}

function base64ToArrayBuffer(b64: string): ArrayBuffer {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes.buffer;
}

Step 6: Test the Crypto Pipeline

typescript
// src/__tests__/e2ee.test.ts
// Verifies encrypt/decrypt roundtrip, sharing, and search

import { describe, it, expect } from 'vitest';
import { deriveMasterKey, generateKeyPair } from '../crypto/key-hierarchy';
import { encryptRecord, decryptRecord } from '../crypto/record-crypto';
import { shareRecord, receiveSharedRecord } from '../crypto/sharing';

describe('E2EE pipeline', () => {
  const salt = new Uint8Array(16);  // fixed for tests

  it('encrypts and decrypts a record', async () => {
    const masterKey = await deriveMasterKey('test-password', salt);
    const record = { patientName: 'Jane Doe', diagnosis: 'Hypertension' };

    const encrypted = await encryptRecord(masterKey, record, ['patientName']);
    const decrypted = await decryptRecord(encrypted, masterKey);

    expect(decrypted).toEqual(record);
  });

  it('generates matching search tokens', async () => {
    const masterKey = await deriveMasterKey('test-password', salt);
    const record = { patientName: 'Jane Doe', diagnosis: 'Hypertension' };

    const encrypted = await encryptRecord(masterKey, record, ['patientName']);

    // Same input produces same token — searchable
    expect(encrypted.searchTokens.length).toBe(1);
    expect(encrypted.searchTokens[0].length).toBeGreaterThan(0);
  });

  it('shares a record between two users', async () => {
    const senderKey = await deriveMasterKey('sender-pass', salt);
    const recipientPair = await generateKeyPair();

    const record = { patientName: 'Jane Doe', notes: 'Sensitive info' };
    const encrypted = await encryptRecord(senderKey, record, []);

    // Share DEK with recipient
    const sharedDek = await shareRecord(
      encrypted.wrappedDek, senderKey, recipientPair.publicKey
    );

    // Recipient decrypts DEK with their private key
    const dek = await receiveSharedRecord(sharedDek, recipientPair.privateKey);

    // Recipient can now decrypt the record
    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: base64ToUint8Array(encrypted.iv) },
      dek,
      base64ToUint8Array(encrypted.ciphertext)
    );

    const result = JSON.parse(new TextDecoder().decode(decrypted));
    expect(result).toEqual(record);
  });
});

Results

After implementing E2EE across the platform:

  • HIPAA audit passed — auditor confirmed zero-knowledge architecture meets breach notification safe harbor
  • 50,000 patient records encrypted client-side, server stores only ciphertext
  • Database breach simulation: extracted DB dump contains zero readable patient data
  • Search latency: blind index lookups take 8ms average — indistinguishable from plaintext search
  • Encryption overhead: 12ms per record encrypt, 8ms decrypt — invisible to users
  • Record sharing works across 3 hospital systems — each with their own key pairs
  • Key recovery tested with 200 users — 100% successful when security answers are correct
  • Insurance premium dropped 15% after demonstrating zero-knowledge architecture
  • Competitor's breach drove 340 new signups in 30 days — E2EE is now the primary sales differentiator

Related use cases