UNPKG

@xnstream/player-sdk

Version:

XStream Player SDK - A powerful video player SDK for streaming content

298 lines 12 kB
import { base64Encode, base64Decode } from "../utils/crypto"; export class LocalStorageSecureStorage { constructor() { this.masterKey = null; this.keyDerivationKey = null; this.KEY_PREFIX = "x_"; this.KEY_STORE_NAME = "xplayer_keystore"; this.KEY_ROTATION_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours this.lastKeyRotation = 0; this.db = null; this.initialized = false; // Private constructor to force use of create() } static async create() { const instance = new LocalStorageSecureStorage(); await instance.initializeKeys(); return instance; } async initializeKeys() { try { await this.initializeKeyStore(); await this.initializeMasterKey(); await this.initializeKeyDerivationKey(); this.lastKeyRotation = Date.now(); this.initialized = true; } catch (error) { console.error("Failed to initialize secure storage:", error); throw error; } } async ensureInitialized() { if (!this.initialized) { await this.initializeKeys(); } } async initializeKeyStore() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.KEY_STORE_NAME, 1); request.onerror = () => { reject(new Error("Failed to open key store")); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains("keys")) { db.createObjectStore("keys", { keyPath: "id" }); } if (!db.objectStoreNames.contains("data")) { db.createObjectStore("data", { keyPath: "id" }); } }; request.onsuccess = (event) => { this.db = event.target.result; if (!this.db) { reject(new Error("Database not initialized")); return; } resolve(); }; }); } async getKeyFromStore(keyId) { if (!this.db) { throw new Error("Database not initialized"); } // Use simple hash for IndexedDB key to avoid recursion const obscureKeyId = this.deriveIndexedDBKey(keyId); // First get the data from the store const result = await new Promise((resolve, reject) => { const transaction = this.db.transaction(["keys"], "readonly"); const store = transaction.objectStore("keys"); const request = store.get(obscureKeyId); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(new Error("Failed to get key from store")); }; transaction.onerror = () => { reject(new Error("Transaction failed")); }; }); if (!result) { return null; } // Then import the key after the transaction is complete try { return await crypto.subtle.importKey("raw", base64Decode(result.keyData), { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); } catch (error) { throw new Error("Failed to import key"); } } async storeKey(keyId, key) { if (!this.db) { throw new Error("Database not initialized"); } // Export the key first, before starting the transaction const exportedKey = await crypto.subtle.exportKey("raw", key); const keyData = base64Encode(new Uint8Array(exportedKey)); // Use simple hash for IndexedDB key to avoid recursion const obscureKeyId = this.deriveIndexedDBKey(keyId); return new Promise((resolve, reject) => { const transaction = this.db.transaction(["keys"], "readwrite"); const store = transaction.objectStore("keys"); // Create the request with obscure key const request = store.put({ id: obscureKeyId, keyData }); // Handle request success request.onsuccess = () => { resolve(); }; // Handle request error request.onerror = () => { reject(new Error("Failed to store key")); }; // Handle transaction error transaction.onerror = () => { reject(new Error("Transaction failed")); }; }); } async initializeMasterKey() { const storedKey = await this.getKeyFromStore("master"); if (!storedKey) { const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); await this.storeKey("master", key); this.masterKey = key; } else { this.masterKey = storedKey; } } async initializeKeyDerivationKey() { const storedKey = await this.getKeyFromStore("derivation"); if (!storedKey) { const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); await this.storeKey("derivation", key); this.keyDerivationKey = key; } else { this.keyDerivationKey = storedKey; } } async rotateKeys() { const now = Date.now(); if (now - this.lastKeyRotation < this.KEY_ROTATION_INTERVAL) { return; } // Generate new keys const newMasterKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); // Re-encrypt all data with new key const oldMasterKey = this.masterKey; this.masterKey = newMasterKey; // Store new key await this.storeKey("master", newMasterKey); this.lastKeyRotation = now; // Clear old key from memory if (oldMasterKey) { await this.clearKeyFromMemory(oldMasterKey); } } async clearKeyFromMemory(key) { // Overwrite key data with zeros const keyData = await crypto.subtle.exportKey("raw", key); const zeroArray = new Uint8Array(keyData.byteLength); const view = new Uint8Array(keyData); view.set(zeroArray); } async deriveObscureKey(key) { if (!this.keyDerivationKey) { await this.initializeKeyDerivationKey(); } // Create a deterministic IV from the key const keyHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key)); const iv = new Uint8Array(keyHash).slice(0, 12); // Encrypt the key name with the derivation key const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, this.keyDerivationKey, new TextEncoder().encode(key)); // Combine IV and encrypted data const result = new Uint8Array(iv.length + encrypted.byteLength); result.set(iv); result.set(new Uint8Array(encrypted), iv.length); // Convert to base64 and take first 16 chars for storage key const base64 = base64Encode(result); return this.KEY_PREFIX + base64.slice(0, 16).replace(/[+/=]/g, "_"); } deriveIndexedDBKey(key) { // Simple hash function for IndexedDB keys to avoid recursion let hash = 0; for (let i = 0; i < key.length; i++) { const char = key.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return this.KEY_PREFIX + Math.abs(hash).toString(36); } async encryptValue(value) { if (!this.masterKey) { await this.initializeMasterKey(); } // Check if key rotation is needed await this.rotateKeys(); const encoder = new TextEncoder(); const data = encoder.encode(value); const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, this.masterKey, data); // Combine IV and encrypted data const result = new Uint8Array(iv.length + encrypted.byteLength); result.set(iv); result.set(new Uint8Array(encrypted), iv.length); return base64Encode(result); } async decryptValue(encryptedValue) { if (!this.masterKey) { await this.initializeMasterKey(); } const encryptedData = base64Decode(encryptedValue); const iv = encryptedData.slice(0, 12); const data = encryptedData.slice(12); const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, this.masterKey, data); const decoder = new TextDecoder(); return decoder.decode(decrypted); } async get(key) { await this.ensureInitialized(); if (!this.db) { throw new Error("Database not initialized"); } const obscureKey = await this.deriveObscureKey(key); return new Promise((resolve, reject) => { const transaction = this.db.transaction(["data"], "readonly"); const store = transaction.objectStore("data"); const request = store.get(obscureKey); request.onsuccess = async () => { const result = request.result; if (!result) { resolve(undefined); return; } try { const decryptedValue = await this.decryptValue(result.value); resolve(decryptedValue); } catch (error) { console.error("Failed to decrypt value:", error); resolve(undefined); } }; request.onerror = () => reject(new Error("Failed to get value from store")); }); } async set(key, value) { await this.ensureInitialized(); if (!this.db) { throw new Error("Database not initialized"); } const obscureKey = await this.deriveObscureKey(key); const encryptedValue = await this.encryptValue(value); return new Promise((resolve, reject) => { const transaction = this.db.transaction(["data"], "readwrite"); const store = transaction.objectStore("data"); const request = store.put({ id: obscureKey, value: encryptedValue }); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error("Failed to set value in store")); }); } async remove(key) { await this.ensureInitialized(); if (!this.db) { throw new Error("Database not initialized"); } const obscureKey = await this.deriveObscureKey(key); return new Promise((resolve, reject) => { const transaction = this.db.transaction(["data"], "readwrite"); const store = transaction.objectStore("data"); const request = store.delete(obscureKey); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error("Failed to remove value from store")); }); } async clear() { await this.ensureInitialized(); // Clear all keys from memory if (this.masterKey) { await this.clearKeyFromMemory(this.masterKey); this.masterKey = null; } if (this.keyDerivationKey) { await this.clearKeyFromMemory(this.keyDerivationKey); this.keyDerivationKey = null; } // Clear all data from IndexedDB const request = indexedDB.deleteDatabase(this.KEY_STORE_NAME); request.onerror = () => { throw new Error("Failed to clear key store"); }; } } //# sourceMappingURL=SecureStorage.js.map