feat(security): encrypt profile passwords at rest with AES-GCM
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.
This commit is contained in:
@@ -100,27 +100,52 @@ function openProfileForm(p) {
|
||||
editingProfileId = p ? p.id : null;
|
||||
$('#pName').value = p ? p.name : '';
|
||||
$('#pUsername').value = p ? p.username : '';
|
||||
$('#pPassword').value = p ? p.password : '';
|
||||
// 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 data = {
|
||||
id: editingProfileId || Date.now().toString(),
|
||||
name: $('#pName').value.trim(),
|
||||
username: $('#pUsername').value.trim(),
|
||||
password: $('#pPassword').value,
|
||||
enabled: $('#pEnabled').checked
|
||||
};
|
||||
if (!data.name || !data.username) return;
|
||||
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] = data;
|
||||
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 {
|
||||
profiles.push(data);
|
||||
const profile = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
username,
|
||||
enabled,
|
||||
passwordEnc
|
||||
};
|
||||
profiles.push(profile);
|
||||
}
|
||||
await saveProfiles(profiles);
|
||||
$('#profileForm').hidden = true;
|
||||
|
||||
Reference in New Issue
Block a user