05/29 Update extension
This commit is contained in:
+91
-14
@@ -24,6 +24,7 @@ let _mfaToken = null;
|
||||
let _currentUrl = "";
|
||||
let _activeTab = "relevant";
|
||||
const _collapsedFolders = new Set(); // persists collapsed state across re-renders
|
||||
let _sharingPrivKey = null; // decrypted ECDH private key for the session
|
||||
|
||||
// ── DOM helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -509,10 +510,36 @@ async function signOut() {
|
||||
await chrome.storage.local.remove(["refresh_token", "enc_key_salt"]);
|
||||
// vault_items_cs is now in session storage — cleared by the session.clear() call above.
|
||||
_vaultKey = null;
|
||||
_sharingPrivKey = null;
|
||||
_items = [];
|
||||
showView("login");
|
||||
}
|
||||
|
||||
// ── Sharing private key ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load and decrypt the ECDH sharing private key into _sharingPrivKey.
|
||||
* No-op if already loaded or if the user hasn't set up sharing keys yet.
|
||||
* Called once per fetchAndDecryptVault() cycle.
|
||||
*/
|
||||
async function _loadSharingPrivKey() {
|
||||
if (_sharingPrivKey) return; // already loaded this session
|
||||
if (!_vaultKey) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/sharing/keys');
|
||||
if (!res?.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data.keys_setup || !data.private_key_enc || !data.private_key_iv) return;
|
||||
_sharingPrivKey = await ExtSharingCrypto.decryptPrivateKey(
|
||||
_vaultKey,
|
||||
data.private_key_enc,
|
||||
data.private_key_iv,
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn('[PassKeeper] Could not load sharing private key:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vault loading ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -596,12 +623,63 @@ async function fetchAndDecryptVault() {
|
||||
}),
|
||||
);
|
||||
|
||||
// Write to session for the popup's own use (badge, rendering).
|
||||
// Write own items to session for popup use (badge, rendering).
|
||||
// vault_items_cs and VAULT_UPDATED are sent after shared items are merged below.
|
||||
await chrome.storage.session.set({ vault_items: _items });
|
||||
|
||||
// Write a lightweight copy to session storage for content scripts.
|
||||
// session storage is memory-only (cleared on browser close) — decrypted
|
||||
// vault data must never be persisted to disk via chrome.storage.local.
|
||||
// Load ECDH sharing private key (no-op if already loaded or keys not set up).
|
||||
await _loadSharingPrivKey();
|
||||
|
||||
// Fetch and decrypt accepted shared items, then merge into _items.
|
||||
// Shared items are treated like own items for autofill/copy but carry
|
||||
// _shared: true and _sharedFrom: ownerEmail for UI differentiation.
|
||||
if (_sharingPrivKey) {
|
||||
try {
|
||||
const inboxRes = await apiFetch('/api/sharing/inbox');
|
||||
if (inboxRes?.ok) {
|
||||
const inbox = await inboxRes.json();
|
||||
const accepted = inbox.filter((s) => s.accepted && s.owner_public_key);
|
||||
const sharedItems = (await Promise.all(
|
||||
accepted.map(async (s) => {
|
||||
try {
|
||||
const ownerPub = await ExtSharingCrypto.importPublicKey(s.owner_public_key);
|
||||
const sharedKey = await ExtSharingCrypto.deriveSharedKey(_sharingPrivKey, ownerPub);
|
||||
const plain = await ExtSharingCrypto.decryptShare(sharedKey, s.enc_data, s.iv);
|
||||
let displayName = s.item_name;
|
||||
if (s.enc_name && s.iv_name) {
|
||||
const dec = await ExtSharingCrypto.decryptName(sharedKey, s.enc_name, s.iv_name);
|
||||
if (dec) displayName = dec;
|
||||
}
|
||||
return {
|
||||
// Use a namespaced id so shared items never collide with own items.
|
||||
id: `shared-${s.id}`,
|
||||
_shareId: s.id,
|
||||
_shared: true,
|
||||
_sharedFrom: s.owner_email || 'Unknown',
|
||||
item_type: s.item_type,
|
||||
name: displayName,
|
||||
folder_id: null,
|
||||
created_at: s.created_at,
|
||||
updated_at: s.created_at,
|
||||
enc_data: s.enc_data,
|
||||
iv: s.iv,
|
||||
plain,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)).filter(Boolean);
|
||||
// Append shared items after own items so they don't displace own-item matches.
|
||||
_items = [..._items, ...sharedItems];
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[PassKeeper] Could not load shared items:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the merged list (own + shared) to session for badge/content script use.
|
||||
// Shared items use their own namespaced ids so the content script can use them.
|
||||
const itemsForContentScript = _items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
@@ -609,11 +687,9 @@ async function fetchAndDecryptVault() {
|
||||
plain: item.plain,
|
||||
}));
|
||||
await chrome.storage.session.set({ vault_items_cs: itemsForContentScript });
|
||||
|
||||
// Notify background to refresh badges and forward to content scripts.
|
||||
chrome.runtime
|
||||
.sendMessage({
|
||||
type: "VAULT_UPDATED",
|
||||
type: 'VAULT_UPDATED',
|
||||
vault_items: itemsForContentScript,
|
||||
})
|
||||
.catch(() => { });
|
||||
@@ -640,7 +716,7 @@ async function fetchAndDecryptVault() {
|
||||
async function _runPopupHealthCheck() {
|
||||
try {
|
||||
const pwItems = _items.filter(
|
||||
(i) => i.item_type === "password" && i.plain?.password,
|
||||
(i) => !i._shared && i.item_type === "password" && i.plain?.password,
|
||||
);
|
||||
if (!pwItems.length) {
|
||||
chrome.runtime
|
||||
@@ -883,6 +959,7 @@ function renderList() {
|
||||
const site = escHtml(siteLabel(item));
|
||||
const name = escHtml(item.name);
|
||||
const badge = matched ? '<span class="badge-match">match</span>' : "";
|
||||
const sharedBadge = item._shared ? `<span class="badge-shared" title="Shared by ${escHtml(item._sharedFrom)}">shared</span>` : "";
|
||||
const color = avatarColor(item.name);
|
||||
const emoji = itemEmoji(item.item_type);
|
||||
const canFill =
|
||||
@@ -902,7 +979,7 @@ function renderList() {
|
||||
return `<div class="vault-item" data-id="${item.id}">
|
||||
<div class="item-avatar ${color}">${emoji}</div>
|
||||
<div class="item-info">
|
||||
<div class="item-site">${site}${badge}${isFav ? '<span class="pk-fav">★</span>' : ""}</div>
|
||||
<div class="item-site">${site}${badge}${sharedBadge}${isFav ? '<span class="pk-fav">★</span>' : ""}</div>
|
||||
<div class="item-name">${name}</div>
|
||||
${tagHtml ? `<div class="pk-tag-row">${tagHtml}</div>` : ""}
|
||||
${hasTotp ? `<div class="item-totp-row"><span class="totp-code-inline" id="totp-${item.id}">······</span><span class="totp-timer-inline" id="totp-t-${item.id}"></span></div>` : ""}
|
||||
@@ -996,7 +1073,7 @@ function renderList() {
|
||||
listEl.querySelectorAll("[data-copy-pass]").forEach((btn) =>
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyPass));
|
||||
const item = _items.find((i) => String(i.id) === btn.dataset.copyPass);
|
||||
if (!item?.plain?.password) return;
|
||||
if (item.plain?.reprompt) {
|
||||
const ok = await _repromptMasterPassword();
|
||||
@@ -1014,7 +1091,7 @@ function renderList() {
|
||||
listEl.querySelectorAll("[data-copy-totp]").forEach((btn) =>
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyTotp));
|
||||
const item = _items.find((i) => String(i.id) === btn.dataset.copyTotp);
|
||||
if (!item?.plain?.totp_uri) return;
|
||||
const code = await getTotpCode(item.plain.totp_uri).catch(() => null);
|
||||
if (code) {
|
||||
@@ -1035,7 +1112,7 @@ function renderList() {
|
||||
listEl.querySelectorAll("[data-autofill]").forEach((btn) =>
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.autofill));
|
||||
const item = _items.find((i) => String(i.id) === btn.dataset.autofill);
|
||||
if (!item?.plain) return;
|
||||
if (item.plain?.reprompt) {
|
||||
const ok = await _repromptMasterPassword();
|
||||
@@ -1063,7 +1140,7 @@ function renderList() {
|
||||
listEl.querySelectorAll("[data-copy-user]").forEach((btn) =>
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.copyUser));
|
||||
const item = _items.find((i) => String(i.id) === btn.dataset.copyUser);
|
||||
if (!item?.plain?.username) return;
|
||||
if (item.plain?.reprompt) {
|
||||
const ok = await _repromptMasterPassword();
|
||||
@@ -1084,7 +1161,7 @@ function renderList() {
|
||||
// Close any already-open flyout first.
|
||||
document.querySelectorAll(".pk-flyout").forEach((el) => el.remove());
|
||||
|
||||
const item = _items.find((i) => i.id === parseInt(btn.dataset.menu));
|
||||
const item = _items.find((i) => String(i.id) === btn.dataset.menu);
|
||||
if (!item) return;
|
||||
|
||||
const flyout = document.createElement("div");
|
||||
|
||||
Reference in New Issue
Block a user