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
| Function | Signature | Description |
|---|---|---|
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:
| Parameter | Type | Default | Description |
|---|---|---|---|
password | string | required | The passphrase to derive from |
salt | string | Uint8Array | random | Optional salt — omit to generate a new random salt |
iterations | number | 100000 | PBKDF2 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); // trueKey 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
| Method | Signature | Description |
|---|---|---|
Keystore.isSupported() | () => boolean | Check 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 | () => void | Clear passphrase from memory |
isUnlocked | () => boolean | Check if keystore is unlocked |
getPassphrase | () => string | Get current passphrase (throws if locked) |
getIterations | () => number | Get 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
| Pattern | Description | Example |
|---|---|---|
email | Email addresses | john@example.com |
phone | Phone numbers | 555-123-4567 |
ssn | Social Security numbers | 123-45-6789 |
creditCard | Credit card numbers | 4111-1111-1111-1111 |
ip | IP addresses | 192.168.1.1 |
address | Street addresses | 123 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
- Never store passwords - Use key derivation with
deriveEncryptionKey() - Unique salts - Omit the salt parameter to auto-generate random salts
- High iterations - Use at least 100,000 PBKDF2 iterations
- Redact PII - Always redact before processing user data
- 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()); // falseDP 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:
| Mechanism | Privacy Guarantee | Best For |
|---|---|---|
| Gaussian | (epsilon, delta)-DP | General use, continuous data |
| Laplacian | Pure epsilon-DP | When 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-corpAudit 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!
});
},
}),
]);