UNPKG

locked-storage

Version:

A JavaScript library that provides cryptographically encrypted local and session storage with automatic key/value encryption and synchronous API

332 lines (291 loc) 9.78 kB
/** * LockedStorage - Cryptographically encrypted localStorage and sessionStorage * @version 2.0.0 * @license MIT */ // Import crypto-js for browser AES encryption let CryptoJS; // Try to load crypto-js in all environments try { CryptoJS = require('crypto-js'); } catch (e) { // In browser without require, CryptoJS should be loaded globally if (typeof window !== 'undefined' && window.CryptoJS) { CryptoJS = window.CryptoJS; } else { CryptoJS = null; } } class LockedStorage { constructor(masterKey, storageType = 'localStorage') { if (!masterKey) { throw new Error('Master key is required for LockedStorage initialization'); } this.masterKey = masterKey; this.storageType = storageType; this.storage = this._getStorage(); this.crypto = this._getCrypto(); // In Node.js environment, we'll use a Map as fallback instead of throwing if (!this.storage && !this._isBrowser()) { // Node.js environment - use Map as storage this._nodeStorage = new Map(); } else if (!this.storage) { throw new Error(`${storageType} is not available in this environment`); } } _isBrowser() { return typeof window !== 'undefined' && typeof window.document !== 'undefined'; } _getStorage() { if (this._isBrowser()) { return this.storageType === 'sessionStorage' ? window.sessionStorage : window.localStorage; } // For Node.js environments, return null - we'll handle this in the constructor return null; } _getCrypto() { if (this._isBrowser()) { if (!CryptoJS) { throw new Error('crypto-js library is required for browser environments. Please include it via script tag or module system.'); } return CryptoJS; } else if (typeof require !== 'undefined') { try { return require('crypto'); } catch (e) { throw new Error('Crypto module not available'); } } throw new Error('No crypto implementation available'); } _deriveKey(salt) { if (this._isBrowser()) { // Browser environment - use crypto-js PBKDF2 const saltWordArray = CryptoJS.lib.WordArray.create(salt); return CryptoJS.PBKDF2(this.masterKey, saltWordArray, { keySize: 256/32, // 256 bits = 8 words of 32 bits each iterations: 100000, hasher: CryptoJS.algo.SHA256 }); } else { // Node.js environment const crypto = this.crypto; return crypto.pbkdf2Sync(this.masterKey, salt, 100000, 32, 'sha256'); } } _encryptData(data) { const salt = this._generateSalt(); const iv = this._generateIV(); if (this._isBrowser()) { // Browser environment - use crypto-js AES-CBC with HMAC const key = this._deriveKey(salt); const ivWordArray = CryptoJS.lib.WordArray.create(iv); const encrypted = CryptoJS.AES.encrypt(data, key, { iv: ivWordArray, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // Add HMAC for authentication const hmacKey = CryptoJS.PBKDF2(this.masterKey + 'hmac', CryptoJS.lib.WordArray.create(salt), { keySize: 256/32, iterations: 10000, hasher: CryptoJS.algo.SHA256 }); const hmac = CryptoJS.HmacSHA256(encrypted.toString(), hmacKey); return { salt: Array.from(salt), iv: Array.from(iv), data: encrypted.toString(), hmac: hmac.toString(CryptoJS.enc.Hex) }; } else { // Node.js environment const crypto = this.crypto; const key = this._deriveKey(salt); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { salt: Array.from(salt), iv: Array.from(iv), data: encrypted, authTag: Array.from(authTag) }; } } _decryptData(encryptedObj) { const salt = new Uint8Array(encryptedObj.salt); const iv = new Uint8Array(encryptedObj.iv); if (this._isBrowser()) { // Browser environment - use crypto-js AES-CBC with HMAC verification const key = this._deriveKey(salt); const ivWordArray = CryptoJS.lib.WordArray.create(iv); // Verify HMAC first const hmacKey = CryptoJS.PBKDF2(this.masterKey + 'hmac', CryptoJS.lib.WordArray.create(salt), { keySize: 256/32, iterations: 10000, hasher: CryptoJS.algo.SHA256 }); const expectedHmac = CryptoJS.HmacSHA256(encryptedObj.data, hmacKey); if (expectedHmac.toString(CryptoJS.enc.Hex) !== encryptedObj.hmac) { throw new Error('Authentication failed - data may have been tampered with'); } const decrypted = CryptoJS.AES.decrypt(encryptedObj.data, key, { iv: ivWordArray, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); } else { // Node.js environment const crypto = this.crypto; const key = this._deriveKey(salt); const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(Buffer.from(encryptedObj.authTag)); let decrypted = decipher.update(encryptedObj.data, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } } _generateSalt() { if (this._isBrowser()) { // Use crypto-js random generation return new Uint8Array(CryptoJS.lib.WordArray.random(16).words.flatMap(word => [ (word >>> 24) & 0xff, (word >>> 16) & 0xff, (word >>> 8) & 0xff, word & 0xff ])); } else { return this.crypto.randomBytes(16); } } _generateIV() { if (this._isBrowser()) { // Use crypto-js random generation return new Uint8Array(CryptoJS.lib.WordArray.random(12).words.flatMap(word => [ (word >>> 24) & 0xff, (word >>> 16) & 0xff, (word >>> 8) & 0xff, word & 0xff ]).slice(0, 12)); } else { return this.crypto.randomBytes(12); } } _generateStorageKey(key) { // Generate a deterministic hash of the key for consistent storage key generation if (this._isBrowser()) { // Browser environment - use crypto-js hash const hash = CryptoJS.SHA256(this.masterKey + key); return 'ls_' + hash.toString(CryptoJS.enc.Hex); } else { // Node.js environment const crypto = this.crypto; const hash = crypto.createHash('sha256'); hash.update(this.masterKey + key); return 'ls_' + hash.digest('hex'); } } setItem(key, value) { try { const storageKey = this._generateStorageKey(key); const encryptedValue = this._encryptData(value); const serializedValue = JSON.stringify(encryptedValue); if (this.storage) { this.storage.setItem(storageKey, serializedValue); } else { // Fallback for Node.js - could be extended to use file system if (!this._nodeStorage) this._nodeStorage = new Map(); this._nodeStorage.set(storageKey, serializedValue); } } catch (error) { throw new Error(`Failed to set item: ${error.message}`); } } getItem(key) { try { const storageKey = this._generateStorageKey(key); let serializedValue; if (this.storage) { serializedValue = this.storage.getItem(storageKey); } else { // Fallback for Node.js if (!this._nodeStorage) return null; serializedValue = this._nodeStorage.get(storageKey); } if (!serializedValue) return null; const encryptedValue = JSON.parse(serializedValue); return this._decryptData(encryptedValue); } catch (error) { throw new Error(`Failed to get item: ${error.message}`); } } removeItem(key) { try { const storageKey = this._generateStorageKey(key); if (this.storage) { this.storage.removeItem(storageKey); } else { // Fallback for Node.js if (this._nodeStorage) { this._nodeStorage.delete(storageKey); } } } catch (error) { throw new Error(`Failed to remove item: ${error.message}`); } } clear() { if (this.storage) { // Only clear items that were set by locked-storage (prefixed with 'ls_') const keys = []; for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); if (key && key.startsWith('ls_')) { keys.push(key); } } keys.forEach(key => this.storage.removeItem(key)); } else { // Fallback for Node.js if (this._nodeStorage) { this._nodeStorage.clear(); } } } } // Factory functions for easy instantiation function createLockedLocalStorage(masterKey) { return new LockedStorage(masterKey, 'localStorage'); } function createLockedSessionStorage(masterKey) { return new LockedStorage(masterKey, 'sessionStorage'); } // Export for different module systems if (typeof module !== 'undefined' && module.exports) { // CommonJS module.exports = { LockedStorage, createLockedLocalStorage, createLockedSessionStorage }; } else if (typeof define === 'function' && define.amd) { // AMD define([], function() { return { LockedStorage, createLockedLocalStorage, createLockedSessionStorage }; }); } else if (typeof window !== 'undefined') { // Browser global window.LockedStorage = { LockedStorage, createLockedLocalStorage, createLockedSessionStorage }; } // Note: ES6 exports are handled by modern bundlers through the module.exports above