@xnstream/player-sdk
Version:
XStream Player SDK - A powerful video player SDK for streaming content
298 lines • 12 kB
JavaScript
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