04/26 Enhanced app and extension security
This commit is contained in:
+56
-18
@@ -87,11 +87,26 @@ function currentHostname() {
|
||||
try { return new URL(_currentUrl).hostname.replace(/^www\./, ''); } catch { return ''; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a stored URL so it is always parseable by `new URL()`.
|
||||
* Handles bare domains ("github.com"), protocol-relative, and full URLs.
|
||||
*/
|
||||
function _normaliseUrl(raw) {
|
||||
if (!raw) return null;
|
||||
const s = raw.trim();
|
||||
if (/^https?:\/\//i.test(s)) return s;
|
||||
if (s.startsWith('//')) return 'https:' + s;
|
||||
return 'https://' + s;
|
||||
}
|
||||
|
||||
function isMatch(item) {
|
||||
const host = currentHostname();
|
||||
if (!host || item.item_type !== 'password' || !item.plain?.url) return false;
|
||||
try {
|
||||
const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
|
||||
const normalised = _normaliseUrl(item.plain.url);
|
||||
if (!normalised) return false;
|
||||
const h = new URL(normalised).hostname.replace(/^www\./, '');
|
||||
// Match exact domain or any subdomain relationship.
|
||||
return h === host || h.endsWith(`.${host}`) || host.endsWith(`.${h}`);
|
||||
} catch { return false; }
|
||||
}
|
||||
@@ -263,7 +278,8 @@ async function signOut() {
|
||||
}
|
||||
} catch { }
|
||||
await chrome.storage.session.clear();
|
||||
await chrome.storage.local.remove(['refresh_token', 'enc_key_salt', 'vault_items_cs']);
|
||||
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; _items = [];
|
||||
showView('login');
|
||||
}
|
||||
@@ -312,23 +328,30 @@ async function fetchAndDecryptVault() {
|
||||
_items = await Promise.all(raw.map(async item => {
|
||||
try {
|
||||
const plain = await ExtCrypto.decryptItem(_vaultKey, item.enc_data, item.iv);
|
||||
return { ...item, plain };
|
||||
// Decrypt the item name if an encrypted version exists.
|
||||
// Fall back to the server-stored plaintext name for legacy items.
|
||||
let displayName = item.name;
|
||||
if (item.enc_name && item.iv_name) {
|
||||
const decrypted = await ExtCrypto.decryptName(_vaultKey, item.enc_name, item.iv_name);
|
||||
if (decrypted) displayName = decrypted;
|
||||
}
|
||||
return { ...item, name: displayName, plain };
|
||||
} catch { return { ...item, plain: null }; }
|
||||
}));
|
||||
|
||||
// Write to session for the popup's own use (badge, rendering).
|
||||
await chrome.storage.session.set({ vault_items: _items });
|
||||
|
||||
// Write a lightweight copy to local storage — this is what content scripts
|
||||
// read, since chrome.storage.local works reliably across all Chrome versions
|
||||
// and does not require message delivery from the background service worker.
|
||||
// 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.
|
||||
const itemsForContentScript = _items.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
item_type: item.item_type,
|
||||
plain: item.plain,
|
||||
}));
|
||||
await chrome.storage.local.set({ vault_items_cs: itemsForContentScript });
|
||||
await chrome.storage.session.set({ vault_items_cs: itemsForContentScript });
|
||||
|
||||
// Notify background to refresh badges and forward to content scripts.
|
||||
chrome.runtime.sendMessage({
|
||||
@@ -658,13 +681,14 @@ async function saveCredential(data) {
|
||||
const folderVal = $('save-folder').value;
|
||||
const folder_id = folderVal ? parseInt(folderVal, 10) : null;
|
||||
const { enc_data, iv } = await ExtCrypto.encryptItem(_vaultKey, plain);
|
||||
const { enc_name, iv_name } = await ExtCrypto.encryptName(_vaultKey, name);
|
||||
try {
|
||||
const res = await apiFetch('/api/vault', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, item_type: 'password', folder_id, enc_data, iv }),
|
||||
body: JSON.stringify({ name: 'password', item_type: 'password', folder_id, enc_data, iv, enc_name, iv_name }),
|
||||
});
|
||||
if (res?.ok) {
|
||||
console.log('[PassKeeper] Credential saved to vault, folder_id:', folder_id);
|
||||
console.log('[PassKeeper] Credential saved to vault:', name, '(name encrypted), folder_id:', folder_id);
|
||||
await fetchAndDecryptVault();
|
||||
renderList();
|
||||
}
|
||||
@@ -680,6 +704,18 @@ const GEN_SETS = {
|
||||
symbols: '!@#$%^&*()-_=+[]{}|;:,.<>?',
|
||||
};
|
||||
|
||||
/**
|
||||
* Cryptographically secure random integer in [0, max).
|
||||
* Uses crypto.getRandomValues exclusively — Math.random() is never called.
|
||||
*/
|
||||
function _cryptoRandInt(max) {
|
||||
// Rejection sampling to eliminate modulo bias.
|
||||
const limit = Math.floor(0x100000000 / max) * max;
|
||||
const buf = new Uint32Array(1);
|
||||
do { crypto.getRandomValues(buf); } while (buf[0] >= limit);
|
||||
return buf[0] % max;
|
||||
}
|
||||
|
||||
function generatePassword(length, useLower, useUpper, useNumbers, useSymbols) {
|
||||
// Always fall back to lower if nothing selected, preventing infinite loop.
|
||||
const pool = [
|
||||
@@ -690,12 +726,13 @@ function generatePassword(length, useLower, useUpper, useNumbers, useSymbols) {
|
||||
].join('');
|
||||
if (!pool) return '';
|
||||
|
||||
// Guarantee at least one character from each selected charset.
|
||||
// Guarantee at least one character from each selected charset
|
||||
// using cryptographically secure random selection.
|
||||
const required = [];
|
||||
if (useLower) required.push(GEN_SETS.lower[Math.floor(Math.random() * GEN_SETS.lower.length)]);
|
||||
if (useUpper) required.push(GEN_SETS.upper[Math.floor(Math.random() * GEN_SETS.upper.length)]);
|
||||
if (useNumbers) required.push(GEN_SETS.numbers[Math.floor(Math.random() * GEN_SETS.numbers.length)]);
|
||||
if (useSymbols) required.push(GEN_SETS.symbols[Math.floor(Math.random() * GEN_SETS.symbols.length)]);
|
||||
if (useLower) required.push(GEN_SETS.lower[_cryptoRandInt(GEN_SETS.lower.length)]);
|
||||
if (useUpper) required.push(GEN_SETS.upper[_cryptoRandInt(GEN_SETS.upper.length)]);
|
||||
if (useNumbers) required.push(GEN_SETS.numbers[_cryptoRandInt(GEN_SETS.numbers.length)]);
|
||||
if (useSymbols) required.push(GEN_SETS.symbols[_cryptoRandInt(GEN_SETS.symbols.length)]);
|
||||
|
||||
const arr = new Uint32Array(length);
|
||||
crypto.getRandomValues(arr);
|
||||
@@ -704,9 +741,9 @@ function generatePassword(length, useLower, useUpper, useNumbers, useSymbols) {
|
||||
// Splice required chars into random positions and trim to length.
|
||||
const combined = [...rest];
|
||||
required.forEach((ch, i) => { combined[i] = ch; });
|
||||
// Fisher-Yates shuffle for uniform distribution.
|
||||
// Fisher-Yates shuffle — fully CSPRNG, no Math.random().
|
||||
for (let i = combined.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const j = _cryptoRandInt(i + 1);
|
||||
[combined[i], combined[j]] = [combined[j], combined[i]];
|
||||
}
|
||||
return combined.slice(0, length).join('');
|
||||
@@ -894,16 +931,17 @@ async function addItemToVault() {
|
||||
try {
|
||||
const plain = { url, username, password, notes };
|
||||
const { enc_data, iv } = await ExtCrypto.encryptItem(_vaultKey, plain);
|
||||
const { enc_name, iv_name } = await ExtCrypto.encryptName(_vaultKey, name);
|
||||
const res = await apiFetch('/api/vault', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, item_type: 'password', folder_id, enc_data, iv }),
|
||||
body: JSON.stringify({ name: 'password', item_type: 'password', folder_id, enc_data, iv, enc_name, iv_name }),
|
||||
});
|
||||
if (!res?.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showError('add-error', data.error || 'Failed to save. Please try again.');
|
||||
return;
|
||||
}
|
||||
console.log('[PassKeeper] Item added to vault:', name);
|
||||
console.log('[PassKeeper] Item added to vault:', name, '(name encrypted)');
|
||||
// Refresh vault and go back.
|
||||
await fetchAndDecryptVault();
|
||||
renderList();
|
||||
|
||||
Reference in New Issue
Block a user