import {initialize, unlock, encryptItem, decryptItem} from './human-crypto.mjs'; import {vaultRequest, VaultUnavailableError} from './human-api.mjs'; import {AUTO_LOCK, policies, VaultSession} from './vault-session.mjs'; const session = new VaultSession(); // Only a boolean, after the caller has validated live ownership. Never expose keys. export const vaultUnlocked = owner => session.owner === owner && !!session.key && !!session.get(owner); // Internal worker callback only. Never register this as a message operation. export async function useVaultCredential(owner, item, use) { const key = session.get(owner), epoch = session.epoch; if (!key) throw new Error(); let account; const check = () => { if (session.epoch !== epoch || !session.get(owner)) throw new Error(); }; try { account = await decryptItem(key, owner, item); check(); return await use(account, check); } finally { if (account) { account.username = ''; account.password = ''; } account = null; } } export async function vaultUsername(owner, item) { const key = session.get(owner); if (!key) return null; let account = null; try { account = await decryptItem(key, owner, item); return account.username; } finally { if (account) { account.username = ''; account.password = ''; account = null; } } } let heartbeat, expiry; function stop() { session.clear(); clearInterval(heartbeat); clearTimeout(expiry); heartbeat = null; } function keepAlive() { clearInterval(heartbeat); if (session.policy !== 'close') expiry = setTimeout(stop, Math.max(0, session.deadline - Date.now())); // Bounded Chrome API heartbeat; worker loss still fails closed. Never persist a key. heartbeat = setInterval(() => { if (!session.get(session.owner)) { stop(); return; } chrome.runtime.getPlatformInfo().catch(stop); }, 20000); } chrome.runtime.onConnect.addListener(port => { if (port.name !== 'human-vault' || port.sender?.id !== chrome.runtime.id || port.sender?.url !== chrome.runtime.getURL('popup.html')) { port.disconnect(); return; } let closed = false, cancellation = 0, queue = Promise.resolve(); port.onDisconnect.addListener(() => { closed = true; session.closed(); }); const handle = async (message, generation) => { if (closed) return; let result; const epoch = session.epoch; const check = () => { if (closed || epoch !== session.epoch) throw new Error(); }; try { if (generation !== cancellation) throw new Error(); if (message.op === 'lock') { stop(); result = true; } else { if (!await chrome.permissions.contains({origins: ['https://persimmons.app/*']})) throw new Error(); const data = await vaultRequest('GET', message.owner); check(); if (data.owner !== message.owner) throw new Error(); if (message.op === 'status') result = !!session.get(message.owner); else if (message.op === 'unlock') { stop(); const unlockEpoch = session.epoch; const checkUnlock = () => { if (closed || session.epoch !== unlockEpoch) throw new Error(); }; const policy = (await chrome.storage.local.get(AUTO_LOCK))[AUTO_LOCK] || 'close'; if (!policies.includes(policy)) throw new Error(); const storageId = `human-vault:https://persimmons.app:${message.owner}`; let record = (await chrome.storage.local.get(storageId))[storageId]; checkUnlock(); if (!record) { if (data.items.length) throw new Error(); record = await initialize(message.passphrase, message.owner); checkUnlock(); await chrome.storage.local.set({[storageId]: record}); checkUnlock(); } const key = await unlock(message.passphrase, message.owner, record); checkUnlock(); session.set(key, message.owner, policy); keepAlive(); result = true; } else { const key = session.get(message.owner); if (!key) throw new Error(); const activeEpoch = session.epoch; if (message.op === 'encrypt') result = await encryptItem(key, message.owner, message.origin, message.account); else if (message.op === 'decrypt') result = await decryptItem(key, message.owner, message.item); else throw new Error(); if (closed || session.epoch !== activeEpoch || !session.get(message.owner)) throw new Error(); } } if (!closed) port.postMessage({id: message.id, ok: true, result}); } catch (error) { stop(); if (!closed) port.postMessage({id: message.id, ok: false, unavailable: error instanceof VaultUnavailableError}); } finally { message.passphrase = ''; message.account = null; result = null; } }; port.onMessage.addListener(message => { if (message.op === 'lock') { cancellation++; stop(); port.postMessage({id: message.id, ok: true, result: true}); return; } const generation = cancellation; queue = queue.then(() => handle(message, generation)); }); }); chrome.permissions.onRemoved.addListener(stop);