LocalMode
Core

Security

Encryption, PII redaction, key management, and security best practices.

LocalMode provides built-in security utilities for encryption, key management, and PII redaction.

See it in action

Try Encrypted Vault and Document Redactor for working demos of these APIs.

Zero Telemetry

LocalMode has zero telemetry. No data ever leaves your device. All processing happens locally in the browser.

Encryption

Encrypt sensitive data using the Web Crypto API. All functions are passphrase-based — no manual key management required:

import { encrypt, decryptString } from '@localmode/core';

// Encrypt data with a passphrase
const encrypted = await encrypt('sensitive data', 'user-password');

// Decrypt back to string
const decrypted = await decryptString(encrypted, 'user-password');
console.log(decrypted); // 'sensitive data'

The encrypt() function returns an EncryptedData object containing everything needed for decryption:

interface EncryptedData {
  /** Base64-encoded encrypted data */
  ciphertext: string;
  /** Base64-encoded initialization vector */
  iv: string;
  /** Key derivation salt */
  salt: string;
  /** Algorithm identifier */
  algorithm: 'AES-GCM';
  /** Version for future compatibility */
  version: 1;
}

Encryption Functions

FunctionSignatureDescription
encrypt(data: string | ArrayBuffer, passphrase: string, iterations?: number) => Promise<EncryptedData>Encrypt string or binary data
decrypt(encrypted: EncryptedData, passphrase: string, iterations?: number) => Promise<ArrayBuffer>Decrypt to ArrayBuffer
decryptString(encrypted: EncryptedData, passphrase: string, iterations?: number) => Promise<string>Decrypt to string
encryptVector(vector: Float32Array, passphrase: string, iterations?: number) => Promise<EncryptedData>Encrypt embedding vectors
decryptVector(encrypted: EncryptedData, passphrase: string, iterations?: number) => Promise<Float32Array>Decrypt embedding vectors
encryptJSON(data: unknown, passphrase: string, iterations?: number) => Promise<EncryptedData>Encrypt JSON-serializable data
decryptJSON(encrypted: EncryptedData, passphrase: string, iterations?: number) => Promise<T>Decrypt JSON data

Key Derivation

For advanced use cases (e.g., encryption middleware), derive a CryptoKey from a passphrase using PBKDF2:

import { deriveEncryptionKey } from '@localmode/core';

// Generate a new key (random salt created automatically)
const { key, salt } = await deriveEncryptionKey('user-password');

// Re-derive the same key later using the stored salt
const { key: sameKey } = await deriveEncryptionKey('user-password', salt);

Parameters:

ParameterTypeDefaultDescription
passwordstringrequiredThe passphrase to derive from
saltstring | Uint8ArrayrandomOptional salt — omit to generate a new random salt
iterationsnumber100000PBKDF2 iterations (higher = more secure, slower)

Returns: Promise<{ key: CryptoKey; salt: string }> — the derived key and the salt used (as base64, for storage).

Always use at least 100,000 iterations for PBKDF2. Lower values make brute-force attacks easier.

Deprecated: deriveKey

The deriveKey export is a deprecated alias for deriveEncryptionKey. Use deriveEncryptionKey in new code.

Passphrase Hashing

Verify passphrases without storing them:

import { hashPassphrase, verifyPassphrase } from '@localmode/core';

const hash = await hashPassphrase('user-password');
const isValid = await verifyPassphrase('user-password', hash); // true

Key Management

The Keystore class manages encryption keys in IndexedDB, handling passphrase verification, lock/unlock lifecycle, and metadata tracking:

import { createKeystore } from '@localmode/core';

const keystore = createKeystore();

// Initialize encryption for a database
await keystore.initialize('my-vectors', 'user-password');

// Later, unlock with the passphrase
const valid = await keystore.unlock('my-vectors', 'user-password');

if (valid) {
  const passphrase = keystore.getPassphrase();
  // Use passphrase with encrypt()/decrypt() functions
}

// Lock when done (clears passphrase from memory)
keystore.lock();

Keystore Methods

MethodSignatureDescription
Keystore.isSupported()() => booleanCheck if Web Crypto API is available (static)
initialize(dbName: string, passphrase: string, iterations?: number) => Promise<void>Set up encryption for a database
unlock(dbName: string, passphrase: string) => Promise<boolean>Verify passphrase and unlock
lock() => voidClear passphrase from memory
isUnlocked() => booleanCheck if keystore is unlocked
getPassphrase() => stringGet current passphrase (throws if locked)
getIterations() => numberGet PBKDF2 iteration count
getMetadata(dbName: string) => Promise<KeyMetadata | null>Get key metadata
hasEncryption(dbName: string) => Promise<boolean>Check if database has encryption
changePassphrase(dbName: string, old: string, new: string) => Promise<boolean>Change passphrase
disable(dbName: string, passphrase: string) => Promise<boolean>Disable encryption
delete(dbName: string) => Promise<void>Delete key metadata

KeyMetadata

interface KeyMetadata {
  /** Database this key is for */
  dbName: string;
  /** Hash of the passphrase for verification */
  passphraseHash: string;
  /** When the key was created */
  createdAt: number;
  /** When the key was last used */
  lastUsedAt: number;
  /** Number of key derivation iterations */
  iterations: number;
  /** Whether encryption is currently enabled */
  enabled: boolean;
}

Key Storage

Keys stored in IndexedDB are accessible to JavaScript. For sensitive applications, consider using hardware-backed keys via WebAuthn.

Encrypting Embeddings

Encrypt embeddings before storage using middleware:

import { wrapEmbeddingModel, encryptionMiddleware, deriveEncryptionKey } from '@localmode/core';

const { key } = await deriveEncryptionKey('user-password');

const model = wrapEmbeddingModel(baseModel, [encryptionMiddleware({ key })]);

// Embeddings are automatically encrypted
const { embedding } = await embed({ model, value: 'sensitive text' });

PII Redaction

Remove personally identifiable information before processing:

import { redactPII } from '@localmode/core';

const text = 'Contact John at john@example.com or call 555-123-4567';

const redacted = redactPII(text, {
  patterns: ['email', 'phone'],
  replacement: '[REDACTED]',
});

console.log(redacted);
// 'Contact John at [REDACTED] or call [REDACTED]'

Available Patterns

PatternDescriptionExample
emailEmail addressesjohn@example.com
phonePhone numbers555-123-4567
ssnSocial Security numbers123-45-6789
creditCardCredit card numbers4111-1111-1111-1111
ipIP addresses192.168.1.1
addressStreet addresses123 Main St

Custom Patterns

const redacted = redactPII(text, {
  patterns: ['email', 'phone'],
  custom: [
    {
      name: 'employeeId',
      regex: /EMP-\d{6}/g,
    },
  ],
  replacement: (match, pattern) => `[${pattern.toUpperCase()}]`,
});

PII Middleware

Automatically redact PII before embedding:

import { wrapEmbeddingModel, piiRedactionMiddleware } from '@localmode/core';

const model = wrapEmbeddingModel(baseModel, [
  piiRedactionMiddleware({
    patterns: ['email', 'phone', 'ssn'],
    replacement: '[REDACTED]',
  }),
]);

// PII is automatically redacted before embedding
const { embedding } = await embed({
  model,
  value: 'Email me at john@example.com',
});
// Actually embeds: 'Email me at [REDACTED]'

Feature Detection

Check security feature availability:

import { isCryptoSupported, isCrossOriginIsolated } from '@localmode/core';

if (!isCryptoSupported()) {
  console.warn('Web Crypto API not available');
}

if (!isCrossOriginIsolated()) {
  console.warn('SharedArrayBuffer not available');
}

Security Best Practices

Security Checklist

  1. Never store passwords - Use key derivation with deriveEncryptionKey()
  2. Unique salts - Omit the salt parameter to auto-generate random salts
  3. High iterations - Use at least 100,000 PBKDF2 iterations
  4. Redact PII - Always redact before processing user data
  5. Zero telemetry - LocalMode never phones home

Secure RAG Pipeline

import {
  wrapEmbeddingModel,
  piiRedactionMiddleware,
  encryptionMiddleware,
  deriveEncryptionKey,
} from '@localmode/core';

// Setup secure model
const { key } = await deriveEncryptionKey(userPassword);

const secureModel = wrapEmbeddingModel(baseModel, [
  piiRedactionMiddleware({
    patterns: ['email', 'phone', 'ssn', 'creditCard'],
  }),
  encryptionMiddleware({ key }),
]);

// All embeddings are PII-redacted and encrypted
const { embedding } = await embed({
  model: secureModel,
  value: userInput,
});

Differential Privacy

Add mathematical privacy guarantees to embeddings and classification outputs using calibrated noise mechanisms.

DP Embeddings

Add noise to embedding vectors so that no single input can be identified:

import { embed, wrapEmbeddingModel, dpEmbeddingMiddleware } from '@localmode/core';
import { transformers } from '@localmode/transformers';

const privateModel = wrapEmbeddingModel({
  model: transformers.embedding('Xenova/all-MiniLM-L6-v2'),
  middleware: dpEmbeddingMiddleware({
    epsilon: 1.0,        // Privacy parameter (lower = more private)
    delta: 1e-5,         // Probability of privacy failure
    mechanism: 'gaussian', // or 'laplacian'
  }),
});

// Embeddings now have calibrated noise added
const { embedding } = await embed({
  model: privateModel,
  value: 'sensitive medical record',
});

Choosing Epsilon

Typical values range from 0.1 (strong privacy, more noise) to 10.0 (weak privacy, less noise). Start with epsilon=1.0 and adjust based on your privacy/utility tradeoff.

Privacy Budget

Track cumulative privacy loss across operations:

import { createPrivacyBudget, dpEmbeddingMiddleware, wrapEmbeddingModel } from '@localmode/core';

const budget = await createPrivacyBudget({
  maxEpsilon: 10.0,
  persistKey: 'my-app-budget', // Persists across sessions via IndexedDB
  onExhausted: 'block',       // Throws PrivacyBudgetExhaustedError when exceeded
});

const privateModel = wrapEmbeddingModel({
  model: baseModel,
  middleware: dpEmbeddingMiddleware({ epsilon: 1.0 }, budget),
});

// Each embed call consumes 1.0 epsilon from the budget
await embed({ model: privateModel, value: 'query 1' }); // 9.0 remaining
await embed({ model: privateModel, value: 'query 2' }); // 8.0 remaining

console.log(budget.remaining()); // 8.0
console.log(budget.isExhausted()); // false

DP Classification (Randomized Response)

Apply randomized response to classification outputs for plausible deniability:

import { randomizedResponse, dpClassificationMiddleware } from '@localmode/core';

// Direct usage
const label = randomizedResponse(
  'positive',                           // True label
  ['positive', 'negative', 'neutral'], // All possible labels
  2.0                                   // Epsilon
);

// As middleware
const middleware = dpClassificationMiddleware({
  epsilon: 2.0,
  labels: ['positive', 'negative', 'neutral'],
});

Noise Mechanisms

Two noise mechanisms are available:

MechanismPrivacy GuaranteeBest For
Gaussian(epsilon, delta)-DPGeneral use, continuous data
LaplacianPure epsilon-DPWhen delta=0 is required
import { gaussianNoise, laplacianNoise, addNoise } from '@localmode/core';

// Generate noise directly
const gNoise = gaussianNoise(384, 0.1);   // 384-dim, sigma=0.1
const lNoise = laplacianNoise(384, 0.5);  // 384-dim, scale=0.5

// Add to an embedding
const noisyEmbedding = addNoise(embedding, gNoise);

Combining PII Redaction with DP

For maximum privacy, combine PII redaction (deterministic) with differential privacy (probabilistic):

import {
  wrapEmbeddingModel,
  composeEmbeddingMiddleware,
  piiRedactionMiddleware,
  dpEmbeddingMiddleware,
} from '@localmode/core';

const secureModel = wrapEmbeddingModel({
  model: baseModel,
  middleware: composeEmbeddingMiddleware([
    piiRedactionMiddleware({ emails: true, phones: true }),
    dpEmbeddingMiddleware({ epsilon: 1.0 }),
  ]),
});

Content Security Policy

For maximum security, configure CSP headers:

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'wasm-unsafe-eval'", // Required for WASM
      "worker-src 'self' blob:", // Required for workers
      "connect-src 'self' https://huggingface.co https://cdn-lfs.huggingface.co",
    ].join('; '),
  },
];

Cross-Origin Isolation

Some features require cross-origin isolation:

// Check if isolated
if (crossOriginIsolated) {
  // SharedArrayBuffer available
  // Better performance for workers
}

// Enable via headers:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp

Audit Logging

Log security-relevant events:

import { wrapEmbeddingModel, loggingMiddleware } from '@localmode/core';

const model = wrapEmbeddingModel(baseModel, [
  loggingMiddleware({
    logger: (event) => {
      // Log to secure audit trail
      auditLog.log({
        timestamp: new Date().toISOString(),
        action: 'embedding',
        model: event.modelId,
        inputCount: event.inputCount,
        // Don't log actual input values!
      });
    },
  }),
]);

Next Steps

Showcase Apps

AppDescriptionLinks
Encrypted VaultClient-side encryption with Web Crypto APIDemo · Source
Document RedactorPII detection and differential privacy for embeddingsDemo · Source

On this page