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
JavaScript
/**
* 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