104 lines
4.9 KiB
JavaScript
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);
|