/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const PW = "correct horse battery staple"; const PW2 = "rotated_password_42"; const PW_WRONG = "Tr0ub4dor&3"; // Per-test kek_refs minted in `add_setup` and reused across the file. // Every kek_ref is of the form `lockstore::kek:::`; // tests that need a *fresh* kek_ref for isolation should call // `mintLocalKek()` / `mintPasswordKek()` directly. let KEK_LOCAL = null; let KEK_PASSWORD = null; function getService() { return Cc["@mozilla.org/security/lockstore;1"].getService(Ci.nsILockstore); } function bytes(input) { return new TextEncoder().encode(input); } function str(arr) { return new TextDecoder().decode(new Uint8Array(arr)); } async function mintLocalKek(identifier = "") { return getService().createKek( "local", identifier, "", /* cacheTimeoutMs */ 0 ); } async function mintPasswordKek(password) { return getService().createKek( "password", /* identifier */ "", password, /* cacheTimeoutMs */ 0 ); } // `NS_ERROR_INVALID_ARG` and `NS_ERROR_ILLEGAL_VALUE` are the same // underlying code (0x80070057); the JS error message uses the latter. const INVALID_ARG_RE = /NS_ERROR_(INVALID_ARG|ILLEGAL_VALUE)/; add_setup(async function () { do_get_profile(); KEK_LOCAL = await mintLocalKek(); // Single Password kek_ref reused across tests that need a non-Local // KEK. Minted with the canonical test password `PW`; tests unlock it // on demand via `ls.unlockKek(KEK_PASSWORD, PW, ...)`. KEK_PASSWORD = await mintPasswordKek(PW); // Keepalive collection: remove_kek / delete_dek now orphan-clean // empty KEK records, so without something always wrapping under // KEK_PASSWORD a later test would delete the record and break // every subsequent test that relies on it. A keepalive coll wrapped // under both KEKs sidesteps the cleanup. const ls = getService(); await ls.unlockKek(KEK_PASSWORD, PW, /*timeoutMs*/ 60_000); await ls.createDek("_keepalive", KEK_LOCAL, false); await ls.addKek("_keepalive", KEK_LOCAL, KEK_PASSWORD); await ls.lockKek(KEK_PASSWORD); }); add_task(function test_service_accessible() { const ls = getService(); Assert.ok(ls, "nsILockstore service must be obtainable"); // A freshly-minted Password kek_ref starts locked: createKek persists // the wrapped KEK but does not populate the unlock cache. Assert.ok( !ls.isKekUnlocked(KEK_PASSWORD), "fresh Password kek starts locked" ); }); add_task(async function test_local_key_encrypt_decrypt_roundtrip() { const ls = getService(); await ls.createDek("rt", KEK_LOCAL, false); const plaintext = bytes("hello, lockstore"); const ct = await ls.encrypt("rt", KEK_LOCAL, plaintext); Assert.greater( ct.length, plaintext.length, "ciphertext longer than plaintext" ); const round = await ls.decrypt("rt", KEK_LOCAL, ct); Assert.equal(str(round), "hello, lockstore", "roundtrip equal"); }); add_task(async function test_create_dek_duplicate_rejects() { const ls = getService(); // "rt" already exists from the previous task; recreating must fail. await Assert.rejects( ls.createDek("rt", KEK_LOCAL, false), /NS_ERROR_FAILURE/, "createDek on an existing collection rejects" ); }); add_task(async function test_create_dek_empty_args_rejected() { const ls = getService(); await Assert.rejects( ls.createDek("", KEK_LOCAL, false), INVALID_ARG_RE, "createDek with empty collection rejects" ); await Assert.rejects( ls.createDek("col", "", false), INVALID_ARG_RE, "createDek with empty kekRef rejects" ); }); add_task(async function test_encrypt_yields_unique_ciphertexts() { const ls = getService(); // Same plaintext, same DEK, two encrypt calls — outputs MUST differ. // The cipher uses a random nonce; identical ciphertexts would be a // catastrophic invariant violation. const pt = bytes("repeatable input"); const a = await ls.encrypt("rt", KEK_LOCAL, pt); const b = await ls.encrypt("rt", KEK_LOCAL, pt); Assert.equal(a.length, b.length, "same plaintext → same ciphertext length"); let differs = false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { differs = true; break; } } Assert.ok(differs, "two encryptions of the same plaintext must differ"); // Both round-trip to the same plaintext. Assert.equal(str(await ls.decrypt("rt", KEK_LOCAL, a)), "repeatable input"); Assert.equal(str(await ls.decrypt("rt", KEK_LOCAL, b)), "repeatable input"); }); add_task(async function test_encrypt_empty_args_rejected() { const ls = getService(); await Assert.rejects( ls.encrypt("", KEK_LOCAL, bytes("x")), INVALID_ARG_RE, "encrypt with empty collection rejects" ); await Assert.rejects( ls.encrypt("rt", "", bytes("x")), INVALID_ARG_RE, "encrypt with empty kekRef rejects" ); }); add_task(async function test_encrypt_no_dek_rejected() { const ls = getService(); await Assert.rejects( ls.encrypt("never-created", KEK_LOCAL, bytes("x")), /NS_ERROR_NOT_AVAILABLE/, "encrypt against a collection without a DEK rejects" ); }); add_task(async function test_decrypt_empty_args_rejected() { const ls = getService(); const ct = await ls.encrypt("rt", KEK_LOCAL, bytes("x")); await Assert.rejects( ls.decrypt("", KEK_LOCAL, ct), INVALID_ARG_RE, "decrypt with empty collection rejects" ); await Assert.rejects( ls.decrypt("rt", "", ct), INVALID_ARG_RE, "decrypt with empty kekRef rejects" ); await Assert.rejects( ls.decrypt("rt", KEK_LOCAL, []), INVALID_ARG_RE, "decrypt of empty ciphertext rejects" ); }); add_task(async function test_decrypt_no_dek_rejected() { const ls = getService(); // Build a syntactically plausible ciphertext (>0 bytes) under a // collection that doesn't exist; FFI must reject NotAvailable. await Assert.rejects( ls.decrypt("never-created", KEK_LOCAL, [1, 2, 3, 4, 5, 6, 7, 8, 9]), /NS_ERROR_NOT_AVAILABLE/, "decrypt against a collection without a DEK rejects" ); }); add_task(async function test_decrypt_corrupted_ciphertext_rejects() { const ls = getService(); const ct = await ls.encrypt("rt", KEK_LOCAL, bytes("tamper-me")); // Flip a bit somewhere inside the AEAD-protected region (skip the // cipher-suite tag at index 0 to avoid the early-out that triggers // INVALID_ARG instead of an AEAD failure). const tampered = new Uint8Array(ct); tampered[Math.floor(tampered.length / 2)] ^= 0x01; await Assert.rejects( ls.decrypt("rt", KEK_LOCAL, Array.from(tampered)), /NS_ERROR_/, "tampering with ciphertext bytes must reject" ); }); add_task(async function test_decrypt_truncated_ciphertext_rejects() { const ls = getService(); const ct = await ls.encrypt("rt", KEK_LOCAL, bytes("truncate-me")); // Drop the trailing AEAD tag → decryption must reject. const truncated = ct.slice(0, ct.length - 4); await Assert.rejects( ls.decrypt("rt", KEK_LOCAL, Array.from(truncated)), /NS_ERROR_/, "truncated ciphertext must reject" ); }); add_task(async function test_decrypt_with_wrong_kek_rejects() { const ls = getService(); // Create a collection wrapped only under KEK_LOCAL. await ls.createDek("local-only", KEK_LOCAL, false); const ct = await ls.encrypt("local-only", KEK_LOCAL, bytes("wrong-kek")); // Decrypting under a KEK that doesn't wrap this DEK must reject. PP // is not unlocked here either, but the failure mode we care about is // "this KEK doesn't wrap this collection". await Assert.rejects( ls.decrypt("local-only", KEK_PASSWORD, ct), /NS_ERROR_NOT_AVAILABLE/, "decrypt under a KEK that does not wrap the collection rejects" ); // Cleanup so listDeks in later tests stays bounded. await ls.deleteDek("local-only"); }); add_task(async function test_list_deks_and_delete() { const ls = getService(); await ls.createDek("one", KEK_LOCAL, false); await ls.createDek("two", KEK_LOCAL, false); const before = await ls.listDeks(); Assert.ok(before.includes("one")); Assert.ok(before.includes("two")); await ls.deleteDek("one"); const after = await ls.listDeks(); Assert.ok(!after.includes("one"), "deleted collection disappears"); Assert.ok(after.includes("two"), "other collection remains"); // Second delete rejects with NotAvailable. await Assert.rejects( ls.deleteDek("one"), /NS_ERROR_NOT_AVAILABLE/, "second deleteDek rejects" ); await Assert.rejects( ls.deleteDek("never-existed"), /NS_ERROR_NOT_AVAILABLE/, "deleteDek on missing collection rejects" ); }); add_task(async function test_delete_dek_empty_arg_rejected() { const ls = getService(); await Assert.rejects( ls.deleteDek(""), INVALID_ARG_RE, "deleteDek with empty arg rejects" ); }); add_task(async function test_listKeks_round_trip() { const ls = getService(); await ls.createDek("keks-rt", KEK_LOCAL, false); let refs = await ls.listKeks("keks-rt"); Assert.deepEqual( refs, [KEK_LOCAL], "listKeks reports only KEK_LOCAL after createDek" ); // addKek under Password would require unlock setup; use the local // KEK twice as a no-op self-test would fail. Instead, exercise the // unknown-collection rejection path here and the addKek/removeKek // listing changes are pinned in the gtest where we can use the // synthetic test KEK level. In production the round-trip is exercised // end-to-end via lockstore-SDR's Password upgrade. await Assert.rejects( ls.listKeks("never-created"), /NS_ERROR_NOT_AVAILABLE/, "listKeks against an unknown collection rejects NotAvailable" ); await Assert.rejects( ls.listKeks(""), /NS_ERROR_NOT_AVAILABLE/, "listKeks with empty arg rejects with NotAvailable (collection lookup fails)" ); await ls.deleteDek("keks-rt"); }); add_task(async function test_delete_dek_nonexistent_rejects() { const ls = getService(); await Assert.rejects( ls.deleteDek("never-existed-coll"), /NS_ERROR_NOT_AVAILABLE/, "deleteDek on a missing collection rejects" ); }); add_task(async function test_delete_dek_succeeds() { const ls = getService(); await ls.createDek("safe-delete", KEK_LOCAL, false); await ls.deleteDek("safe-delete"); await Assert.rejects( ls.encrypt("safe-delete", KEK_LOCAL, bytes("nope")), /NS_ERROR_NOT_AVAILABLE/, "DEK is gone after deleteDek" ); }); add_task(async function test_delete_dek_nonexistent_rejects() { const ls = getService(); await Assert.rejects( ls.deleteDek("never-existed-coll-2"), /NS_ERROR_NOT_AVAILABLE/, "deleteDek on a missing collection rejects" ); }); add_task(async function test_delete_dek_empty_arg_rejected() { const ls = getService(); await Assert.rejects( ls.deleteDek(""), INVALID_ARG_RE, "deleteDek with empty arg rejects" ); }); add_task(async function test_password_lifecycle() { const ls = getService(); // NOTE: production iterations are 800 000; expect ~1s per // unlockKek call on modern hardware. The unlock runs off-main-thread. Assert.ok(!ls.isKekUnlocked(KEK_PASSWORD), "freshly minted, starts locked"); // Wrong password rejected; cache stays empty. await Assert.rejects( ls.unlockKek(KEK_PASSWORD, PW_WRONG, 60000), /NS_ERROR_ABORT/, "wrong password rejected" ); Assert.ok( !ls.isKekUnlocked(KEK_PASSWORD), "wrong password does not populate cache" ); await ls.unlockKek(KEK_PASSWORD, PW, 60000); Assert.ok(ls.isKekUnlocked(KEK_PASSWORD), "correct password unlocks"); // Encrypt/decrypt under the Password KEK while unlocked. await ls.createDek("pw-col", KEK_PASSWORD, false); const ct = await ls.encrypt("pw-col", KEK_PASSWORD, bytes("secret")); const round = await ls.decrypt("pw-col", KEK_PASSWORD, ct); Assert.equal(str(round), "secret", "Password roundtrip while unlocked"); // Locking invalidates the cache; subsequent encrypt rejects. await ls.lockKek(KEK_PASSWORD); Assert.ok(!ls.isKekUnlocked(KEK_PASSWORD)); await Assert.rejects( ls.encrypt("pw-col", KEK_PASSWORD, bytes("nope")), /NS_ERROR_NOT_AVAILABLE/, "encrypt under locked Password kek rejects with Locked" ); await ls.deleteDek("pw-col"); }); add_task(async function test_multiple_password_keks_unlock_independently() { const ls = getService(); // Two independent password KEKs minted with different passwords. const kekA = await mintPasswordKek(PW); const kekB = await mintPasswordKek(PW2); Assert.notEqual(kekA, kekB, "createKek mints distinct kek_refs each call"); await ls.unlockKek(kekA, PW, 60000); Assert.ok(ls.isKekUnlocked(kekA)); Assert.ok(!ls.isKekUnlocked(kekB), "kek B is unaffected by kek A's unlock"); // PW2 is correct for kekB but wrong for kekA. await Assert.rejects( ls.unlockKek(kekA, PW2, 60000), /NS_ERROR_ABORT/, "PW2 is wrong for kekA" ); await ls.unlockKek(kekB, PW2, 60000); Assert.ok(ls.isKekUnlocked(kekB)); // lockKek scopes per-kek_ref. await ls.lockKek(kekA); Assert.ok(!ls.isKekUnlocked(kekA)); Assert.ok(ls.isKekUnlocked(kekB), "kek B still unlocked after kek A locked"); await ls.lockKek(kekB); }); add_task(async function test_add_remove_kek() { const ls = getService(); // Already unlocked from prior task; re-unlock defensively. if (!ls.isKekUnlocked(KEK_PASSWORD)) { await ls.unlockKek(KEK_PASSWORD, PW, 60000); } await ls.createDek("multi", KEK_LOCAL, true); await ls.addKek("multi", KEK_LOCAL, KEK_PASSWORD); const ct = await ls.encrypt("multi", KEK_LOCAL, bytes("shared DEK")); const round = await ls.decrypt("multi", KEK_PASSWORD, ct); Assert.equal(str(round), "shared DEK", "same DEK decrypts via either KEK"); await ls.removeKek("multi", KEK_PASSWORD); await Assert.rejects( ls.encrypt("multi", KEK_PASSWORD, bytes("nope")), /NS_ERROR_NOT_AVAILABLE/, "removed KEK is gone" ); }); add_task(async function test_remove_last_kek_rejected() { const ls = getService(); // "multi" is now wrapped only under KEK_LOCAL (PP wrapping was // removed in the previous task). Removing the last wrapping must // fail to avoid silently orphaning the DEK. await Assert.rejects( ls.removeKek("multi", KEK_LOCAL), /NS_ERROR_/, "removing the last remaining wrapping must reject" ); // Sanity: collection still usable. const ct = await ls.encrypt("multi", KEK_LOCAL, bytes("still here")); Assert.equal( str(await ls.decrypt("multi", KEK_LOCAL, ct)), "still here", "DEK unchanged after rejected remove" ); }); add_task(async function test_remove_kek_not_present_rejects() { const ls = getService(); // KEK_PASSWORD doesn't wrap "multi" anymore; removing a non-present // wrapping must reject (vs. silently no-op). await Assert.rejects( ls.removeKek("multi", KEK_PASSWORD), /NS_ERROR_/, "removeKek of a kekRef that is not currently wrapping rejects" ); }); add_task(async function test_add_kek_missing_collection_rejects() { const ls = getService(); if (!ls.isKekUnlocked(KEK_PASSWORD)) { await ls.unlockKek(KEK_PASSWORD, PW, 60000); } await Assert.rejects( ls.addKek("never-created-coll", KEK_LOCAL, KEK_PASSWORD), /NS_ERROR_NOT_AVAILABLE/, "addKek against a collection without a DEK rejects" ); }); add_task(async function test_add_kek_empty_args_rejected() { const ls = getService(); await Assert.rejects( ls.addKek("", KEK_LOCAL, KEK_PASSWORD), INVALID_ARG_RE, "addKek with empty collection rejects" ); await Assert.rejects( ls.addKek("multi", "", KEK_PASSWORD), INVALID_ARG_RE, "addKek with empty fromKekRef rejects" ); await Assert.rejects( ls.addKek("multi", KEK_LOCAL, ""), INVALID_ARG_RE, "addKek with empty toKekRef rejects" ); }); add_task(async function test_remove_kek_empty_args_rejected() { const ls = getService(); await Assert.rejects( ls.removeKek("", KEK_LOCAL), INVALID_ARG_RE, "removeKek with empty collection rejects" ); await Assert.rejects( ls.removeKek("multi", ""), INVALID_ARG_RE, "removeKek with empty kekRef rejects" ); }); add_task(async function test_local_key_is_always_unlocked() { const ls = getService(); Assert.ok(ls.isKekUnlocked(KEK_LOCAL), "LocalKey always unlocked"); await ls.lockKek(KEK_LOCAL); // no-op Assert.ok(ls.isKekUnlocked(KEK_LOCAL), "LocalKey still unlocked after lock"); await ls.unlockKek(KEK_LOCAL, "", 60000); // no-op Assert.ok(ls.isKekUnlocked(KEK_LOCAL)); }); add_task(async function test_lock_all() { const ls = getService(); if (!ls.isKekUnlocked(KEK_PASSWORD)) { await ls.unlockKek(KEK_PASSWORD, PW, 60000); } Assert.ok(ls.isKekUnlocked(KEK_PASSWORD), "PP unlocked before lock()"); await ls.lock(); Assert.ok(!ls.isKekUnlocked(KEK_PASSWORD), "PP locked after lock()"); Assert.ok(ls.isKekUnlocked(KEK_LOCAL), "LocalKey unaffected by lock()"); }); add_task(async function test_unknown_kek_ref_rejected() { const ls = getService(); const BOGUS = "lockstore::kek::bogus"; // Every kek_ref-taking method validates the prefix; a malformed // kek_ref surfaces as InvalidKekRef (NS_ERROR_INVALID_ARG). Assert.throws( () => ls.isKekUnlocked(BOGUS), INVALID_ARG_RE, "isKekUnlocked rejects an unknown kek_ref" ); await Assert.rejects( ls.unlockKek(BOGUS, "whatever", 60000), INVALID_ARG_RE, "unlockKek rejects an unknown kek_ref" ); await Assert.rejects( ls.lockKek(BOGUS), INVALID_ARG_RE, "lockKek rejects an unknown kek_ref" ); }); add_task(async function test_empty_kek_ref_rejected() { const ls = getService(); await Assert.rejects( ls.unlockKek("", "pw", 60000), INVALID_ARG_RE, "empty kek_ref rejected by unlockKek" ); await Assert.rejects( ls.lockKek(""), INVALID_ARG_RE, "empty kek_ref rejected by lockKek" ); Assert.throws( () => ls.isKekUnlocked(""), INVALID_ARG_RE, "empty kek_ref rejected by isKekUnlocked" ); }); add_task(async function test_pkcs11_unknown_kek_ref_rejected() { // No PKCS#11 token is registered in the xpcshell harness, so we can // only exercise error paths for the PKCS#11 branch. A kek_ref that // has no persisted Pkcs11KekRecord must reject without touching any // slot. const ls = getService(); const bogusPkcs11 = "lockstore::kek::pkcs11:not-a-real-record"; await Assert.rejects( ls.unlockKek(bogusPkcs11, "pin-bytes", 60000), /NS_ERROR_(NOT_AVAILABLE|INVALID_ARG|ILLEGAL_VALUE|FAILURE)/, "unknown PKCS#11 kek_ref rejected" ); }); // The service serialises FFI on a private queue. Multiple in-flight // async ops must all complete and produce distinct ciphertexts. add_task(async function test_concurrent_encrypts_serialised() { const ls = getService(); await ls.createDek("concurrent", KEK_LOCAL, false); const N = 8; const pt = bytes("parallel-but-serialised"); const cts = await Promise.all( Array.from({ length: N }, () => ls.encrypt("concurrent", KEK_LOCAL, pt)) ); Assert.equal(cts.length, N, "every concurrent encrypt resolved"); // All ciphertexts of the same plaintext must be unique (random nonce). const seen = new Set(); for (const ct of cts) { seen.add(Array.from(ct).join(",")); } Assert.equal( seen.size, N, "every concurrent encrypt produced a unique ciphertext" ); // All decrypt back to the original plaintext. const rounds = await Promise.all( cts.map(ct => ls.decrypt("concurrent", KEK_LOCAL, ct)) ); for (const r of rounds) { Assert.equal(str(r), "parallel-but-serialised"); } await ls.deleteDek("concurrent"); }); add_task(async function test_concurrent_mixed_ops() { const ls = getService(); // Mix createDek / encrypt / listDeks / deleteDek in flight. const colls = ["mix-a", "mix-b", "mix-c"]; await Promise.all(colls.map(c => ls.createDek(c, KEK_LOCAL, false))); const collsAfter = await ls.listDeks(); for (const c of colls) { Assert.ok(collsAfter.includes(c), `${c} present after concurrent create`); } // Encrypt under each in parallel. const cts = await Promise.all( colls.map(c => ls.encrypt(c, KEK_LOCAL, bytes(c))) ); // Decrypt under each in parallel. const rounds = await Promise.all( colls.map((c, i) => ls.decrypt(c, KEK_LOCAL, cts[i])) ); for (let i = 0; i < colls.length; i++) { Assert.equal(str(rounds[i]), colls[i], `${colls[i]} round-trips correctly`); } // Cleanup in parallel. await Promise.all(colls.map(c => ls.deleteDek(c))); const collsFinal = await ls.listDeks(); for (const c of colls) { Assert.ok(!collsFinal.includes(c), `${c} cleaned up`); } }); add_task(async function test_import_dek_roundtrip() { const ls = getService(); // 32 known bytes — pattern, not crypto-quality. const dek = new Uint8Array(32); for (let i = 0; i < dek.length; i++) { dek[i] = (i + 1) & 0xff; } await ls.importDek("imported", KEK_LOCAL, dek, true); const round = await ls.getDek("imported", KEK_LOCAL); Assert.equal(round.length, 32, "exported DEK is 32 bytes"); for (let i = 0; i < 32; i++) { Assert.equal(round[i], dek[i], `byte ${i} round-trips`); } // Sanity: encrypt/decrypt under the imported DEK. const ct = await ls.encrypt("imported", KEK_LOCAL, bytes("payload")); Assert.equal(str(await ls.decrypt("imported", KEK_LOCAL, ct)), "payload"); await ls.deleteDek("imported"); }); add_task(async function test_import_dek_wrong_length_rejected() { const ls = getService(); const short = new Uint8Array(16); await Assert.rejects( ls.importDek("short", KEK_LOCAL, short, true), /NS_ERROR_FAILURE/, "importDek with non-32-byte input rejects (InvalidConfiguration)" ); }); add_task(async function test_import_dek_empty_args_rejected() { const ls = getService(); const dek = new Uint8Array(32); await Assert.rejects( ls.importDek("", KEK_LOCAL, dek, true), INVALID_ARG_RE, "importDek with empty collection rejects" ); await Assert.rejects( ls.importDek("c", "", dek, true), INVALID_ARG_RE, "importDek with empty kekRef rejects" ); await Assert.rejects( ls.importDek("c", KEK_LOCAL, [], true), INVALID_ARG_RE, "importDek with empty dekBytes rejects" ); }); add_task(async function test_import_dek_duplicate_rejected() { const ls = getService(); const dek = new Uint8Array(32); await ls.importDek("dup", KEK_LOCAL, dek, true); await Assert.rejects( ls.importDek("dup", KEK_LOCAL, dek, true), /NS_ERROR_FAILURE/, "importDek on an existing collection rejects" ); await ls.deleteDek("dup"); }); add_task(async function test_is_dek_extractable_true() { const ls = getService(); await ls.createDek("extract-yes", KEK_LOCAL, true); Assert.equal( await ls.isDekExtractable("extract-yes"), true, "isDekExtractable reflects extractable=true at creation" ); await ls.deleteDek("extract-yes"); }); add_task(async function test_is_dek_extractable_false() { const ls = getService(); await ls.createDek("extract-no", KEK_LOCAL, false); Assert.equal( await ls.isDekExtractable("extract-no"), false, "isDekExtractable reflects extractable=false at creation" ); await ls.deleteDek("extract-no"); }); add_task(async function test_is_dek_extractable_missing_rejected() { const ls = getService(); await Assert.rejects( ls.isDekExtractable("never-existed-coll"), /NS_ERROR_NOT_AVAILABLE/, "isDekExtractable on a missing collection rejects" ); }); add_task(async function test_switch_kek_round_trip() { const ls = getService(); if (!ls.isKekUnlocked(KEK_PASSWORD)) { await ls.unlockKek(KEK_PASSWORD, PW, 60000); } await ls.createDek("switch-rt", KEK_LOCAL, false); // Produce ciphertext under the OLD kek_ref; after switching to PP the // same ciphertext must still decrypt — switch_kek changes only the // wrapping, not the DEK bytes. const ct = await ls.encrypt("switch-rt", KEK_LOCAL, bytes("preserved")); await ls.switchKek("switch-rt", KEK_LOCAL, KEK_PASSWORD); const refs = await ls.listKeks("switch-rt"); Assert.deepEqual( refs, [KEK_PASSWORD], "only the new kekRef wraps the collection after switch" ); Assert.equal( str(await ls.decrypt("switch-rt", KEK_PASSWORD, ct)), "preserved", "ciphertext decrypts under the new kekRef (DEK bytes preserved)" ); await Assert.rejects( ls.decrypt("switch-rt", KEK_LOCAL, ct), /NS_ERROR_NOT_AVAILABLE/, "old kekRef no longer wraps the collection" ); await ls.deleteDek("switch-rt"); }); add_task(async function test_switch_kek_same_ref_rejected() { const ls = getService(); await ls.createDek("same-ref", KEK_LOCAL, false); await Assert.rejects( ls.switchKek("same-ref", KEK_LOCAL, KEK_LOCAL), /NS_ERROR_FAILURE/, "switchKek(old == new) rejects as InvalidConfiguration" ); await ls.deleteDek("same-ref"); }); add_task(async function test_switch_kek_empty_args_rejected() { const ls = getService(); await Assert.rejects( ls.switchKek("", KEK_LOCAL, KEK_PASSWORD), INVALID_ARG_RE, "switchKek with empty collection rejects" ); await Assert.rejects( ls.switchKek("c", "", KEK_PASSWORD), INVALID_ARG_RE, "switchKek with empty oldKekRef rejects" ); await Assert.rejects( ls.switchKek("c", KEK_LOCAL, ""), INVALID_ARG_RE, "switchKek with empty newKekRef rejects" ); }); add_task(async function test_switch_kek_missing_collection_rejected() { const ls = getService(); await Assert.rejects( ls.switchKek("never-existed", KEK_LOCAL, KEK_PASSWORD), /NS_ERROR_NOT_AVAILABLE/, "switchKek on a missing collection rejects" ); }); // --------------------------------------------------------------------------- // deleteKek // --------------------------------------------------------------------------- add_task(async function test_delete_kek_drops_unreferenced_local() { const ls = getService(); const ephemeral = await mintLocalKek(); await ls.deleteKek(ephemeral); await Assert.rejects( ls.createDek("dk-after-delete", ephemeral, false), /NS_ERROR/, "createDek under a deleted local kek_ref fails" ); }); add_task(async function test_delete_kek_drops_unreferenced_password() { const ls = getService(); const ephemeral = await mintPasswordKek(PW); await ls.deleteKek(ephemeral); await Assert.rejects( ls.unlockKek(ephemeral, PW, /*timeoutMs*/ 60_000), /NS_ERROR/, "unlockKek on a deleted password kek_ref fails" ); }); add_task(async function test_delete_kek_rejects_when_in_use() { const ls = getService(); const ephemeral = await mintLocalKek(); await ls.createDek("dk-in-use", ephemeral, false); await Assert.rejects( ls.deleteKek(ephemeral), /NS_ERROR_FAILURE/, "deleteKek of a kek_ref that still wraps a DEK is rejected" ); }); add_task(async function test_delete_kek_unknown_ref_rejects() { const ls = getService(); await Assert.rejects( ls.deleteKek("lockstore::kek::local:AAAAAAAAAAAAAAAAAAAAAA"), /NS_ERROR_NOT_AVAILABLE/, "deleteKek on an unknown kek_ref rejects with NOT_AVAILABLE" ); }); add_task(async function test_delete_kek_invalid_arg_rejected() { const ls = getService(); await Assert.rejects( ls.deleteKek(""), INVALID_ARG_RE, "deleteKek with empty kek_ref is rejected" ); await Assert.rejects( ls.deleteKek("not-a-valid-kek-ref"), /NS_ERROR/, "deleteKek with malformed kek_ref is rejected" ); });