SHiNE-server/SHiNE-browser-plugin-wallet/js/lib/session-store.js

153 lines
4.3 KiB
JavaScript

import { base64ToBytes, bytesToBase64 } from './crypto-utils.js';
const DB_NAME = 'shine-wallet-plugin';
const DB_VERSION = 1;
const STORE_META = 'meta';
const STORE_VAULT = 'vault';
const SESSION_ENTRY_ID = 'active-session';
const VAULT_KEY_ID = 'session-wrap-key';
function openDb() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_META)) {
db.createObjectStore(STORE_META, { keyPath: 'id' });
}
if (!db.objectStoreNames.contains(STORE_VAULT)) {
db.createObjectStore(STORE_VAULT, { keyPath: 'id' });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error || new Error('IndexedDB недоступен'));
});
}
async function withStore(storeName, mode, run) {
const db = await openDb();
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction(storeName, mode);
const store = tx.objectStore(storeName);
let settled = false;
const done = (fn) => (value) => {
if (settled) return;
settled = true;
fn(value);
};
tx.oncomplete = () => done(resolve)(undefined);
tx.onerror = () => done(reject)(tx.error || new Error('IndexedDB transaction failed'));
Promise.resolve(run(store, tx, done)).catch((error) => done(reject)(error));
});
} finally {
db.close();
}
}
async function put(storeName, value) {
return withStore(storeName, 'readwrite', (store) => {
store.put(value);
});
}
async function get(storeName, key) {
const db = await openDb();
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const req = tx.objectStore(storeName).get(key);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => reject(req.error || new Error('Ошибка чтения из IndexedDB'));
});
} finally {
db.close();
}
}
async function deleteById(storeName, key) {
return withStore(storeName, 'readwrite', (store) => {
store.delete(key);
});
}
async function getOrCreateVaultKey() {
const current = await get(STORE_META, VAULT_KEY_ID);
if (current?.key) return current.key;
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt'],
);
await put(STORE_META, { id: VAULT_KEY_ID, key, createdAtMs: Date.now() });
return key;
}
async function encryptJson(value) {
const key = await getOrCreateVaultKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const plainBytes = new TextEncoder().encode(JSON.stringify(value));
const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plainBytes);
return {
ivB64: bytesToBase64(iv),
cipherB64: bytesToBase64(new Uint8Array(cipher)),
};
}
async function decryptJson(envelope) {
const key = await getOrCreateVaultKey();
const plain = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToBytes(envelope.ivB64) },
key,
base64ToBytes(envelope.cipherB64),
);
return JSON.parse(new TextDecoder().decode(plain));
}
function storageApi() {
if (globalThis.chrome?.storage?.local) return globalThis.chrome.storage.local;
return null;
}
export async function savePluginSettings(settings) {
const api = storageApi();
if (api) {
await api.set({ shineWalletSettings: settings });
return;
}
localStorage.setItem('shineWalletSettings', JSON.stringify(settings));
}
export async function loadPluginSettings() {
const api = storageApi();
if (api) {
const row = await api.get('shineWalletSettings');
return row?.shineWalletSettings || {};
}
try {
return JSON.parse(localStorage.getItem('shineWalletSettings') || '{}');
} catch {
return {};
}
}
export async function saveSessionMaterial(sessionRecord) {
const encrypted = await encryptJson(sessionRecord);
await put(STORE_VAULT, {
id: SESSION_ENTRY_ID,
encrypted,
updatedAtMs: Date.now(),
});
}
export async function loadSessionMaterial() {
const row = await get(STORE_VAULT, SESSION_ENTRY_ID);
if (!row?.encrypted) return null;
return decryptJson(row.encrypted);
}
export async function clearSessionMaterial() {
await deleteById(STORE_VAULT, SESSION_ENTRY_ID);
}