@bantis/local-cipher
v2.1.0
Published
Client-side encryption for localStorage - Framework-agnostic with React and Angular support
Downloads
501
Maintainers
Keywords
Readme
@bantis/local-cipher
Client-side encryption for localStorage using AES-256-GCM
Protect sensitive data in browser storage from XSS attacks, local file access, and casual inspection. Drop-in replacement for localStorage with automatic encryption/decryption.
Problem
localStorage stores data in plain text. Anyone with access to DevTools, browser files, or malicious scripts can read:
- Authentication tokens
- User credentials
- API keys
- Personal information
Solution
Transparent AES-256-GCM encryption with browser fingerprinting. Data is encrypted before storage and decrypted on retrieval. Keys are derived from browser characteristics, making data unreadable outside the original browser context.
Quick Start
npm install @bantis/local-cipherimport { SecureStorage } from '@bantis/local-cipher';
const storage = SecureStorage.getInstance();
// Store encrypted
await storage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// Retrieve decrypted
const token = await storage.getItem('token');
// Works like localStorage
await storage.removeItem('token');
storage.clear();Before:
localStorage: { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }After:
localStorage: { "__enc_a7f5d8e2": "Qm9keUVuY3J5cHRlZERhdGE..." }Features
- ✅ AES-256-GCM encryption with authentication
- ✅ PBKDF2 key derivation (100k+ iterations)
- ✅ Browser fingerprinting for unique keys per device
- ✅ Key obfuscation - even key names are encrypted
- ✅ TTL/Expiration - auto-delete expired data
- ✅ Event system - monitor storage operations
- ✅ Compression - automatic gzip for large values
- ✅ Namespaces - organize data in isolated spaces
- ✅ Integrity checks - SHA-256 checksums
- ✅ TypeScript - full type definitions
- ✅ Framework support - React hooks, Angular service
Use Cases
1. Protect Authentication Tokens
// Store JWT with 1-hour expiration
await storage.setItemWithExpiry('accessToken', jwt, {
expiresIn: 3600000
});
// Auto-cleanup expired tokens
storage.on('expired', ({ key }) => {
console.log(`Token ${key} expired, redirecting to login`);
window.location.href = '/login';
});2. Secure User Preferences
const userStorage = storage.namespace('user');
await userStorage.setItem('theme', 'dark');
await userStorage.setItem('language', 'en');
// Isolated from other namespaces
const appStorage = storage.namespace('app');3. Cache Sensitive API Responses
// Store with compression for large data
const storage = SecureStorage.getInstance({
storage: { compression: true, compressionThreshold: 512 }
});
await storage.setItem('userData', JSON.stringify(largeObject));React Integration
import { useSecureStorage, useSecureStorageEvents } from '@bantis/local-cipher';
function App() {
const [token, setToken, loading] = useSecureStorage('token', '');
useSecureStorageEvents('expired', () => {
// Handle expiration
});
if (loading) return <div>Loading...</div>;
return <div>Token: {token}</div>;
}🚀 Quick Start
JavaScript Vanilla
import { SecureStorage } from '@bantis/local-cipher';
const storage = SecureStorage.getInstance();
// Store encrypted
await storage.setItem('accessToken', 'mi-token-secreto');
// Retrieve decrypted
const token = await storage.getItem('accessToken');
// With expiration (1 hour)
await storage.setItemWithExpiry('session', sessionData, { expiresIn: 3600000 });
// Remove
await storage.removeItem('accessToken');Before:
localStorage: { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }After:
localStorage: { "__enc_a7f5d8e2": "Qm9keUVuY3J5cHRlZERhdGE..." }React
import { useSecureStorage } from '@bantis/local-cipher/react';
function App() {
const [token, setToken, loading] = useSecureStorage('accessToken', '');
if (loading) return <div>Loading...</div>;
return (
<div>
<p>Token: {token}</p>
<button onClick={() => setToken('nuevo-token')}>
Update Token
</button>
</div>
);
}Angular
import { SecureStorageService } from '@bantis/local-cipher/angular';
@Component({
selector: 'app-root',
template: `<div>{{ token$ | async }}</div>`
})
export class AppComponent {
token$ = this.storage.getItem('accessToken');
constructor(private storage: SecureStorageService) {}
saveToken(token: string) {
this.storage.setItem('accessToken', token).subscribe();
}
}Angular Integration
import { SecureStorageService } from '@bantis/local-cipher';
@Component({...})
export class AppComponent {
token$ = this.storage.getItem('token');
constructor(private storage: SecureStorageService) {
this.storage.events$.subscribe(event => {
console.log('Storage event:', event);
});
}
}Configuration
const storage = SecureStorage.getInstance({
encryption: {
iterations: 150000, // PBKDF2 iterations
keyLength: 256, // 128, 192, or 256 bits
saltLength: 16, // Salt size in bytes
ivLength: 12, // IV size in bytes
},
storage: {
compression: true, // Enable gzip compression
compressionThreshold: 1024, // Compress if > 1KB
autoCleanup: true, // Auto-delete expired items
cleanupInterval: 60000 // Cleanup every 60s
},
debug: {
enabled: false, // Enable debug logging
logLevel: 'info' // silent, error, warn, info, debug, verbose
}
});Security
What This Protects Against
✅ XSS attacks - Encrypted data is useless without the browser-specific key
✅ Local file access - Malware reading browser files gets encrypted data
✅ Casual inspection - DevTools shows encrypted values
✅ Data tampering - Integrity checks detect modifications
What This Does NOT Protect Against
❌ Server-side attacks - Encryption is client-side only
❌ Man-in-the-Middle - Use HTTPS for data in transit
❌ Memory dumps - Keys exist in memory during runtime
❌ Compromised browser - If the browser is compromised, all bets are off
❌ Physical access during active session - Data is decrypted when accessed
Best Practices
- Use HTTPS - Always transmit data over secure connections
- Short TTLs - Set expiration on sensitive data
- Clear on logout - Call
storage.clear()when user logs out - Monitor events - Track suspicious activity via event listeners
- Rotate keys - Periodically call
storage.rotateKeys() - Don't store passwords - Never store plaintext passwords, even encrypted
Browser Support
Requires Web Crypto API:
- Chrome 37+
- Firefox 34+
- Safari 11+
- Edge 12+
- Opera 24+
Fallback: Gracefully degrades to unencrypted localStorage in unsupported browsers.
API Reference
Core Methods
setItem(key: string, value: string): Promise<void>
getItem(key: string): Promise<string | null>
removeItem(key: string): Promise<void>
hasItem(key: string): Promise<boolean>
clear(): voidExpiration
setItemWithExpiry(key: string, value: string, options: {
expiresIn?: number; // milliseconds from now
expiresAt?: Date; // absolute date
}): Promise<void>
cleanExpired(): Promise<number> // Returns count of deleted itemsEvents
on(event: StorageEventType, listener: EventListener): void
off(event: StorageEventType, listener: EventListener): void
once(event: StorageEventType, listener: EventListener): void
// Event types: 'encrypted', 'decrypted', 'deleted', 'cleared',
// 'expired', 'error', 'keyRotated', 'compressed'Namespaces
namespace(name: string): NamespacedStorage
const userStorage = storage.namespace('user');
await userStorage.setItem('profile', data);
await userStorage.clearNamespace();Key Rotation
rotateKeys(): Promise<void>
exportEncryptedData(): Promise<EncryptedBackup>
importEncryptedData(backup: EncryptedBackup): Promise<void>FAQ
Q: Is this secure enough for passwords?
A: No. Never store passwords in localStorage, even encrypted. Use secure, httpOnly cookies or sessionStorage with server-side session management.
Q: Can data be decrypted on another device?
A: No. Keys are derived from browser fingerprinting. Data encrypted on Chrome/Windows cannot be decrypted on Firefox/Mac.
Q: What happens if Web Crypto API is unavailable?
A: The library falls back to unencrypted localStorage with a console warning. Check EncryptionHelper.isSupported() to detect support.
Q: Does this protect against XSS?
A: Partially. It makes stolen data harder to use, but XSS can still intercept data when it's decrypted in memory. Use CSP headers and sanitize inputs.
Q: How is this different from sessionStorage?
A: sessionStorage is cleared on tab close. This provides persistent, encrypted storage across sessions.
Q: Can I use this in Node.js?
A: No. This library requires browser APIs (Web Crypto, localStorage). For Node.js, use native crypto module.
Q: What's the performance impact?
A: Encryption adds ~2-5ms per operation. Compression adds ~5-10ms for large values. Negligible for most use cases.
Migration from v1
v1 data is automatically migrated to v2 format on first read. No action required.
// v1 and v2 are API-compatible
const storage = SecureStorage.getInstance(); // Works with bothMigration from v2.0.x to v2.1.0
[!WARNING] Breaking Change: Framework-specific imports required
For React Users
Before (v2.0.x):
import { useSecureStorage } from '@bantis/local-cipher';After (v2.1.0):
import { useSecureStorage } from '@bantis/local-cipher/react';For Angular Users
Before (v2.0.x):
import { SecureStorageService } from '@bantis/local-cipher';After (v2.1.0):
import { SecureStorageService } from '@bantis/local-cipher/angular';For Core/Vanilla JS Users
No changes required:
import { SecureStorage } from '@bantis/local-cipher'; // Still worksWhy This Change?
v2.0.x bundled all framework code together, causing dependency conflicts:
- React projects needed Angular dependencies (
@angular/core,rxjs) - Vanilla JS projects loaded unused React/Angular code
- 40% larger bundle size for non-framework users
v2.1.0 separates frameworks into independent bundles:
- ✅ Core: 42KB (no framework dependencies)
- ✅ React: 49KB (core + hooks)
- ✅ Angular: 55KB (core + service)
