Files
app/extension/vault-worker.mjs
T
2026-10-04 02:46:52 +00:00

104 lines
4.9 KiB
JavaScript

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);