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.

Need tamper-evident provenance?

For append-only, hash-chained, signed audit logs (HIPAA / SOX / FedRAMP-aligned), see Audit Log. It complements the encryption helpers here: chain integrity is verifiable without the encryption key.

See it in action

Try the Encrypted Vault block for a working demo 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 Stored Documents

encryptionMiddleware is a VectorDB middleware: wrap the database with wrapVectorDB. It encrypts each document's metadata before it reaches storage and decrypts it on get() and in search results:

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

const { key, salt } = await deriveEncryptionKey('user-password');
// Persist `salt` (it is not secret) to derive the same key next session

const db = wrapVectorDB({
  db: await createVectorDB({ name: 'notes', dimensions: 384 }),
  middleware: encryptionMiddleware({
    key,
    excludeFields: ['category'], // stays plaintext so it can be filtered on
  }),
});

await db.add({ id: 'n1', vector: embedding, metadata: { text: 'sensitive text', category: 'medical' } });
const results = await db.search(queryVector, { k: 5 }); // metadata decrypted
OptionTypeDefaultDescription
keyCryptoKeyrequiredAES-GCM key, e.g. from deriveEncryptionKey()
encryptMetadatabooleantrueEncrypt metadata fields
encryptTextbooleantrueEncrypt string metadata values. With false, strings stay plaintext (and filterable) while numbers, booleans, and objects are still encrypted
excludeFieldsstring[][]Metadata fields left in plaintext
encryptVectorsbooleanfalseMust be omitted or false; true throws a ValidationError

Vectors stay in plaintext

The index computes distances over the stored vectors, so it can neither store nor search ciphertext. To keep embeddings from leaking sensitive content, redact PII before embedding (below) and consider differential privacy. To protect vectors at rest outside the live index, encrypt an exported snapshot with encryptVector().

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, {
  emails: true,
  phones: true,
  replacement: '[REDACTED]',
});

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

Available Categories

OptionDefaultDescriptionExampleDefault replacement
emailstrueEmail addressesjohn@example.com[EMAIL_REDACTED]
phonestruePhone numbers555-123-4567[PHONE_REDACTED]
ssntrueSocial Security numbers123-45-6789[SSN_REDACTED]
creditCardstrueCredit card numbers4111-1111-1111-1111[CARD_REDACTED]
ipAddressesfalseIP addresses192.168.1.1[IP_REDACTED]
datesfalseDates01/15/2024[DATE_REDACTED]

replacement overrides every category; replacements (e.g. { email: '<email>' }) overrides individual ones.

Custom Patterns

const redacted = redactPII(text, {
  emails: true,
  phones: true,
  customPatterns: [{ pattern: /EMP-\d{6}/g, replacement: '[EMPLOYEE_ID]' }],
});

PII Middleware

Automatically redact PII before embedding:

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

const model = wrapEmbeddingModel({
  model: baseModel,
  middleware: piiRedactionMiddleware({ emails: true, phones: true, ssn: true }),
});

// PII is automatically redacted before embedding
const { embedding } = await embed({
  model,
  value: 'Email me at john@example.com',
});
// Actually embeds: 'Email me at [EMAIL_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 {
  embed,
  createVectorDB,
  wrapEmbeddingModel,
  wrapVectorDB,
  piiRedactionMiddleware,
  encryptionMiddleware,
  deriveEncryptionKey,
} from '@localmode/core';

const { key } = await deriveEncryptionKey(userPassword, storedSalt);

// Embedding model: redact PII before embedding
const secureModel = wrapEmbeddingModel({
  model: baseModel,
  middleware: piiRedactionMiddleware({ emails: true, phones: true, ssn: true, creditCards: true }),
});

// Vector DB: encrypt metadata before storage
const db = wrapVectorDB({
  db: await createVectorDB({ name: 'secure-rag', dimensions: 384 }),
  middleware: encryptionMiddleware({ key }),
});

// The embedding is computed from PII-redacted text; the stored text is encrypted
const { embedding } = await embed({ model: secureModel, value: userInput });
await db.add({ id: crypto.randomUUID(), vector: embedding, metadata: { text: 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 VectorDB operations with loggingMiddleware (a VectorDB middleware; it logs IDs, counts, and timings, never vectors or metadata values):

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

const db = wrapVectorDB({
  db: baseDB,
  middleware: loggingMiddleware({
    operations: ['add', 'delete', 'clear'],
    formatter: (operation, data) => JSON.stringify({ at: new Date().toISOString(), operation, ...data }),
    logger: (line) => securityLog.push(String(line)),
  }),
});

For a tamper-evident, hash-chained record, see Audit Log.

Next Steps

Blocks

AppDescriptionLinks
Privacy (Encrypted Vault)Client-side encryption with Web Crypto APILive block · Source
Privacy (PII Redactor)PII detection and differential privacy for embeddingsLive block · Source

On this page