Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
const { ProfileKekPassword } = ChromeUtils.importESModule(
"resource://gre/modules/ProfileKekPassword.sys.mjs"
);
const PREF = "security.lockstore.unlock.enabled";
// Duplicated deliberately, so a change to the module's own refs fails here;
// keep in step with ProfileKekPassword.sys.mjs and ProfileKek.h.
const KEK_ID = "profile";
const PW_KEK = `lockstore::kek::password:${KEK_ID}`;
const LOCAL_KEK = `lockstore::kek::local:${KEK_ID}`;
const PW = "correct horse battery staple";
const PW2 = "rotated_password_42";
function getLockstore() {
return Cc["@mozilla.org/security/lockstore;1"].getService(Ci.nsILockstore);
}
// The module keys everything off one fixed KEK id, so every task reuses the
// same records. Delete them between tasks, DEKs first: deleteKek refuses
// while any DEK is still wrapped under the KEK.
async function resetLockstore() {
const ls = getLockstore();
for (const dekName of await ls.listDeks()) {
await ls.deleteDek(dekName);
}
for (const kekRef of [PW_KEK, LOCAL_KEK]) {
if (await ls.kekExists(kekRef)) {
await ls.deleteKek(kekRef);
}
}
}
async function createLocalKek() {
return getLockstore().createKek("local", KEK_ID, "", 0);
}
async function createPasswordKek(password) {
return getLockstore().createKek(
"password",
KEK_ID,
password,
Number.MAX_SAFE_INTEGER
);
}
async function assertRejects(promise, message) {
let threw = false;
try {
await promise;
} catch (e) {
threw = true;
}
Assert.ok(threw, message);
}
add_setup(async function () {
do_get_profile();
Services.prefs.setBoolPref(PREF, true);
registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
});
add_task(async function test_exists_false_when_pref_disabled() {
await resetLockstore();
await createPasswordKek(PW);
Services.prefs.setBoolPref(PREF, false);
Assert.ok(
!(await ProfileKekPassword.exists()),
"exists() reports false with the link disabled even though the KEK is there"
);
Services.prefs.setBoolPref(PREF, true);
Assert.ok(
await ProfileKekPassword.exists(),
"exists() reports the KEK once the link is enabled"
);
});
add_task(async function test_exists_false_without_kek() {
await resetLockstore();
Assert.ok(
!(await ProfileKekPassword.exists()),
"exists() is false with no password KEK"
);
});
add_task(async function test_update_is_a_noop_when_pref_disabled() {
await resetLockstore();
Services.prefs.setBoolPref(PREF, false);
await ProfileKekPassword.update("", PW);
Services.prefs.setBoolPref(PREF, true);
Assert.ok(
!(await getLockstore().kekExists(PW_KEK)),
"no password KEK is minted while the link is disabled"
);
});
add_task(async function test_update_sets_first_password() {
await resetLockstore();
await ProfileKekPassword.update("", PW);
const ls = getLockstore();
Assert.ok(await ls.kekExists(PW_KEK), "password KEK created");
Assert.ok(
ls.isKekUnlocked(PW_KEK),
"the freshly created KEK is unlocked for the session"
);
});
add_task(async function test_update_empty_to_empty_is_a_noop() {
await resetLockstore();
await ProfileKekPassword.update("", "");
const ls = getLockstore();
Assert.ok(!(await ls.kekExists(PW_KEK)), "no password KEK minted");
Assert.ok(!(await ls.kekExists(LOCAL_KEK)), "no local KEK minted");
});
add_task(async function test_update_migrates_existing_deks_onto_password() {
await resetLockstore();
const ls = getLockstore();
await createLocalKek();
await ls.createDek("places.sqlite", LOCAL_KEK, true, 32);
const before = await ls.getDek("places.sqlite", LOCAL_KEK);
await ProfileKekPassword.update("", PW);
Assert.ok(await ls.kekExists(PW_KEK), "password KEK created");
Assert.ok(
!(await ls.kekExists(LOCAL_KEK)),
"the emptied local KEK is dropped, so the DEK is no longer readable " +
"without the primary password"
);
Assert.deepEqual(
await ls.listKeks("places.sqlite"),
[PW_KEK],
"the DEK is wrapped under the password KEK alone"
);
Assert.deepEqual(
await ls.getDek("places.sqlite", PW_KEK),
before,
"the DEK bytes survive the migration"
);
});
add_task(async function test_update_changes_password_and_keeps_deks() {
await resetLockstore();
const ls = getLockstore();
await createPasswordKek(PW);
await ls.createDek("places.sqlite", PW_KEK, true, 32);
const before = await ls.getDek("places.sqlite", PW_KEK);
await ProfileKekPassword.update(PW, PW2);
Assert.ok(await ls.kekExists(PW_KEK), "the password KEK is still there");
Assert.ok(
ls.isKekUnlocked(PW_KEK),
"the session unlock is restored after the change"
);
Assert.deepEqual(
await ls.getDek("places.sqlite", PW_KEK),
before,
"the DEK bytes survive the password change"
);
await ls.lockKek(PW_KEK);
await assertRejects(
ls.unlockKek(PW_KEK, PW, 60_000),
"the old password no longer unlocks the KEK"
);
await ls.unlockKek(PW_KEK, PW2, 60_000);
});
add_task(async function test_update_wrong_old_password_rejects() {
await resetLockstore();
const ls = getLockstore();
await createPasswordKek(PW);
await ls.lockKek(PW_KEK);
await assertRejects(
ProfileKekPassword.update("not-the-password", PW2),
"update() surfaces the failed unlock rather than rewrapping"
);
await ls.unlockKek(PW_KEK, PW, 60_000);
Assert.ok(
await ls.kekExists(PW_KEK),
"the KEK is still keyed to the original password"
);
});
add_task(async function test_update_removes_password_and_falls_back_to_local() {
await resetLockstore();
const ls = getLockstore();
await createPasswordKek(PW);
await ls.createDek("places.sqlite", PW_KEK, true, 32);
const before = await ls.getDek("places.sqlite", PW_KEK);
await ProfileKekPassword.update(PW, "");
Assert.ok(
!(await ls.kekExists(PW_KEK)),
"the emptied password KEK is dropped"
);
Assert.ok(await ls.kekExists(LOCAL_KEK), "the local KEK took over");
Assert.deepEqual(
await ls.getDek("places.sqlite", LOCAL_KEK),
before,
"the DEK bytes survive the rollback, so the profile stays readable"
);
});
add_task(async function test_discard_unlocked_preserves_deks() {
await resetLockstore();
const ls = getLockstore();
await createPasswordKek(PW);
await ls.createDek("places.sqlite", PW_KEK, true, 32);
const before = await ls.getDek("places.sqlite", PW_KEK);
Assert.ok(ls.isKekUnlocked(PW_KEK), "precondition: the KEK is unlocked");
await ProfileKekPassword.discard();
Assert.ok(!(await ls.kekExists(PW_KEK)), "the password KEK is gone");
Assert.deepEqual(
await ls.getDek("places.sqlite", LOCAL_KEK),
before,
"an unlocked discard moves the DEKs to the local KEK instead of " +
"destroying them"
);
});
add_task(async function test_discard_locked_drops_unrecoverable_deks() {
await resetLockstore();
const ls = getLockstore();
await createPasswordKek(PW);
await createLocalKek();
await ls.createDek("places.sqlite", PW_KEK, true, 32);
await ls.createDek("cookies.sqlite", LOCAL_KEK, true, 32);
const survivor = await ls.getDek("cookies.sqlite", LOCAL_KEK);
await ls.lockKek(PW_KEK);
await ProfileKekPassword.discard();
Assert.ok(!(await ls.kekExists(PW_KEK)), "the password KEK is gone");
Assert.ok(
!(await ls.dekExists("places.sqlite")),
"a DEK nothing can unwrap again is deleted"
);
Assert.ok(
await ls.dekExists("cookies.sqlite"),
"a DEK under the local KEK is untouched"
);
Assert.deepEqual(
await ls.getDek("cookies.sqlite", LOCAL_KEK),
survivor,
"the surviving DEK is still readable"
);
});
add_task(async function test_discard_ignores_the_pref() {
await resetLockstore();
const ls = getLockstore();
await createPasswordKek(PW);
Services.prefs.setBoolPref(PREF, false);
await ProfileKekPassword.discard();
Services.prefs.setBoolPref(PREF, true);
// A KEK that outlived the pref going off is exactly the one the reset
// leaves keyed to a destroyed password, so discard() ignores the pref.
Assert.ok(
!(await ls.kekExists(PW_KEK)),
"discard() drops the password KEK even with the link disabled"
);
});
add_task(async function test_discard_without_kek_is_a_noop() {
await resetLockstore();
await ProfileKekPassword.discard();
Assert.ok(
!(await getLockstore().kekExists(PW_KEK)),
"discard() with nothing to discard does nothing"
);
});
// The helpers below take the lockstore as an argument, so their failure paths
// can be driven with a stub rather than by breaking the real keystore.
add_task(async function test_moveDeks_rolls_back_the_target_on_failure() {
const deleted = [];
const stub = {
migrateDeks: () => Promise.reject(new Error("migrate failed")),
deleteKek: kekRef => {
deleted.push(kekRef);
return Promise.resolve();
},
};
await assertRejects(
ProfileKekPassword.moveDeks(stub, PW_KEK, LOCAL_KEK),
"a failed migration is reported to the caller"
);
Assert.deepEqual(
deleted,
[LOCAL_KEK],
"only the empty target KEK is dropped; the source still holds the DEKs"
);
});
add_task(async function test_moveDeks_drops_the_emptied_source() {
const deleted = [];
const stub = {
migrateDeks: () => Promise.resolve(),
deleteKek: kekRef => {
deleted.push(kekRef);
return Promise.resolve();
},
};
await ProfileKekPassword.moveDeks(stub, PW_KEK, LOCAL_KEK);
Assert.deepEqual(deleted, [PW_KEK], "the emptied source KEK is dropped");
});
add_task(async function test_moveDeks_survives_a_failed_cleanup() {
const stub = {
migrateDeks: () => Promise.resolve(),
deleteKek: () => Promise.reject(new Error("delete failed")),
};
// A leftover source KEK is harmless; it must not turn a completed
// migration into a reported failure.
await ProfileKekPassword.moveDeks(stub, PW_KEK, LOCAL_KEK);
Assert.ok(true, "moveDeks resolved despite the cleanup failure");
});
add_task(async function test_changePassword_skips_unlock_when_locked() {
const calls = [];
const stub = {
isKekUnlocked: () => false,
changeKekPassword: () => {
calls.push("change");
return Promise.resolve();
},
unlockKek: () => {
calls.push("unlock");
return Promise.resolve();
},
};
await ProfileKekPassword.changePassword(stub, PW, PW2);
Assert.deepEqual(
calls,
["change"],
"a KEK that was locked to begin with is not unlocked as a side effect"
);
});
add_task(async function test_changePassword_tolerates_failed_unlock() {
const stub = {
isKekUnlocked: () => true,
changeKekPassword: () => Promise.resolve(),
unlockKek: () => Promise.reject(new Error("unlock failed")),
};
// A failed restore leaves the KEK locked, which no longer concerns the
// password change itself: the next startup prompts and unlocks it.
await ProfileKekPassword.changePassword(stub, PW, PW2);
Assert.ok(true, "changePassword resolved despite the failed relock");
});