Passwords are now encrypted with Web Crypto AES-GCM (256-bit) before being stored in browser.storage.local. The random key is persisted so it survives browser/service-worker restarts (otherwise old ciphertext could never be decrypted). Encryption/decryption happen in the background via encrypt-password/decrypt-password messages; autoLogin decrypts on the fly and falls back to a legacy plaintext password for pre-encryption profiles. The popup never reveals a stored password and preserves it when editing with a blank field. Removes the plaintext-password-at-rest smell flagged in the design review. Note: this is at-rest obfuscation-level security (the key ships with the extension); the extension only talks to the school portal.
161 lines
5.7 KiB
JavaScript
161 lines
5.7 KiB
JavaScript
'use strict';
|
|
|
|
// Cross-browser API namespace (see background.js). No-op in Firefox.
|
|
if (typeof browser === 'undefined' || browser == null) {
|
|
var browser = (typeof chrome !== 'undefined' ? chrome : {});
|
|
}
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
|
|
const MASTER_KEY = 'enabled';
|
|
const PROFILES_KEY = 'profiles';
|
|
|
|
let editingProfileId = null;
|
|
|
|
// ---- Master toggle ----
|
|
async function loadMaster() {
|
|
const { enabled } = await browser.storage.local.get(MASTER_KEY);
|
|
$('#masterToggle').checked = enabled !== false;
|
|
$('#masterLabel').textContent = enabled === false ? 'Disabled' : 'Enabled';
|
|
}
|
|
$('#masterToggle').addEventListener('change', async (e) => {
|
|
await browser.storage.local.set({ [MASTER_KEY]: e.target.checked });
|
|
$('#masterLabel').textContent = e.target.checked ? 'Enabled' : 'Disabled';
|
|
});
|
|
|
|
// ---- PDF settings: truncate long course descriptions (default ON) ----
|
|
async function loadPdfSettings() {
|
|
const s = await browser.storage.local.get(['truncateDescription', 'showYearLevel', 'customYearLevel', 'yearMode']);
|
|
$('#truncateToggle').checked = s.truncateDescription !== false;
|
|
$('#showYearToggle').checked = s.showYearLevel !== false;
|
|
$('#customYear').value = s.customYearLevel || '';
|
|
$('#yearMode').value = s.yearMode || 'auto';
|
|
}
|
|
$('#truncateToggle').addEventListener('change', async (e) => {
|
|
await browser.storage.local.set({ truncateDescription: e.target.checked });
|
|
});
|
|
$('#showYearToggle').addEventListener('change', async (e) => {
|
|
await browser.storage.local.set({ showYearLevel: e.target.checked });
|
|
});
|
|
$('#yearMode').addEventListener('change', async (e) => {
|
|
await browser.storage.local.set({ yearMode: e.target.value });
|
|
});
|
|
$('#customYear').addEventListener('change', async (e) => {
|
|
await browser.storage.local.set({ customYearLevel: e.target.value.trim() });
|
|
});
|
|
|
|
// ---- Profiles ----
|
|
async function getProfiles() {
|
|
const { profiles } = await browser.storage.local.get(PROFILES_KEY);
|
|
return profiles || [];
|
|
}
|
|
async function saveProfiles(profiles) {
|
|
await browser.storage.local.set({ [PROFILES_KEY]: profiles });
|
|
}
|
|
|
|
function renderProfiles(profiles) {
|
|
const list = $('#profileList');
|
|
list.innerHTML = '';
|
|
for (const p of profiles) {
|
|
const li = document.createElement('li');
|
|
const name = document.createElement('div');
|
|
name.className = 'name';
|
|
name.innerHTML = '<strong>' + escapeHtml(p.name) + '</strong><br><span class="user">' + escapeHtml(p.username) + '</span>';
|
|
const toggle = document.createElement('input');
|
|
toggle.type = 'checkbox';
|
|
toggle.checked = !!p.enabled;
|
|
toggle.title = 'Enable/disable';
|
|
toggle.addEventListener('change', async () => {
|
|
if (toggle.checked) {
|
|
profiles.forEach((x) => { x.enabled = x.id === p.id; });
|
|
} else {
|
|
p.enabled = false;
|
|
}
|
|
await saveProfiles(profiles);
|
|
renderProfiles(await getProfiles());
|
|
});
|
|
const edit = document.createElement('button');
|
|
edit.className = 'btn';
|
|
edit.textContent = 'Edit';
|
|
edit.addEventListener('click', () => openProfileForm(p));
|
|
const del = document.createElement('button');
|
|
del.className = 'btn';
|
|
del.textContent = 'Delete';
|
|
del.addEventListener('click', async () => {
|
|
await saveProfiles(profiles.filter((x) => x.id !== p.id));
|
|
renderProfiles(await getProfiles());
|
|
});
|
|
li.append(toggle, name, edit, del);
|
|
list.appendChild(li);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[c]));
|
|
}
|
|
|
|
function openProfileForm(p) {
|
|
editingProfileId = p ? p.id : null;
|
|
$('#pName').value = p ? p.name : '';
|
|
$('#pUsername').value = p ? p.username : '';
|
|
// The stored password is encrypted and not revealed; leave the field blank.
|
|
// On save, a blank password preserves the existing one.
|
|
$('#pPassword').value = '';
|
|
$('#pEnabled').checked = p ? !!p.enabled : true;
|
|
$('#profileForm').hidden = false;
|
|
if (p) $('#pPassword').placeholder = 'Leave blank to keep existing password';
|
|
else $('#pPassword').placeholder = 'Password';
|
|
}
|
|
$('#addProfile').addEventListener('click', () => openProfileForm(null));
|
|
$('#cancelProfile').addEventListener('click', () => { $('#profileForm').hidden = true; });
|
|
$('#saveProfile').addEventListener('click', async () => {
|
|
const profiles = await getProfiles();
|
|
const name = $('#pName').value.trim();
|
|
const username = $('#pUsername').value.trim();
|
|
const typedPassword = $('#pPassword').value;
|
|
const enabled = $('#pEnabled').checked;
|
|
if (!name || !username) return;
|
|
|
|
// Encrypt a newly-typed password via the background.
|
|
let passwordEnc = null;
|
|
if (typedPassword) {
|
|
const res = await browser.runtime.sendMessage({ type: 'encrypt-password', password: typedPassword });
|
|
if (!res || !res.ok) { alert('Could not encrypt password: ' + ((res && res.error) || 'unknown error')); return; }
|
|
passwordEnc = res.cipher;
|
|
}
|
|
|
|
if (editingProfileId) {
|
|
const i = profiles.findIndex((x) => x.id === editingProfileId);
|
|
if (i >= 0) {
|
|
profiles[i].name = name;
|
|
profiles[i].username = username;
|
|
profiles[i].enabled = enabled;
|
|
// Only replace the password if a new one was typed; otherwise keep the
|
|
// existing (already encrypted) one.
|
|
if (passwordEnc) profiles[i].passwordEnc = passwordEnc;
|
|
delete profiles[i].password; // legacy plaintext no longer needed
|
|
}
|
|
} else {
|
|
const profile = {
|
|
id: Date.now().toString(),
|
|
name,
|
|
username,
|
|
enabled,
|
|
passwordEnc
|
|
};
|
|
profiles.push(profile);
|
|
}
|
|
await saveProfiles(profiles);
|
|
$('#profileForm').hidden = true;
|
|
renderProfiles(profiles);
|
|
});
|
|
|
|
// ---- Init ----
|
|
(async function init() {
|
|
await loadMaster();
|
|
await loadPdfSettings();
|
|
renderProfiles(await getProfiles());
|
|
})();
|