153 lines
4.3 KiB
JavaScript
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);
|
|
}
|