Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Tests for the passphrase protection tab of the OpenPGP key details
* dialog, and for the actions it offers depending on the pref
* mail.openpgp.passphrases.enabled and on how the key is actually
* protected. The pref only controls whether we offer to set a separate
* passphrase, it doesn't change how already existing secret keys are
* protected, so a key may be protected by a user-chosen passphrase even
* when the pref is disabled.
*/
"use strict";
const { OpenPGPTestUtils } = ChromeUtils.importESModule(
);
const { EnigmailWindows } = ChromeUtils.importESModule(
);
const ALICE_KEY_ID = "0xf231550c4f47e38e";
const ALICE_PASSPHRASE = "alice-passphrase";
// Ofelia's secret key was exported with "gpg --export-secret-subkeys",
// so the secret key material of the primary key is missing.
const OFELIA_KEY_ID = "0x97DCDA5E56EBB822";
const PASSPHRASE_PREF = "mail.openpgp.passphrases.enabled";
function keyFile(name) {
return new FileUtils.File(getTestFilePath(`data/keys/${name}`));
}
/**
* Import Alice's secret key, which is protected by the automatic
* passphrase that Thunderbird manages.
*/
function importAutoProtectedKey() {
return OpenPGPTestUtils.importPrivateKey(
window,
keyFile("alice@openpgp.example-0xf231550c4f47e38e-secret.asc")
);
}
/**
* Import Alice's secret key, keeping its user-chosen passphrase.
*/
function importPassphraseProtectedKey() {
return OpenPGPTestUtils.importPrivateKey(
window,
keyFile("alice@openpgp.example-0xf231550c4f47e38e-secret-with-pp.asc"),
OpenPGPTestUtils.ACCEPTANCE_PERSONAL,
ALICE_PASSPHRASE,
true
);
}
/**
* Open the key details dialog for the given key, the same way the key
* manager does, and run the given function once the dialog has detected
* the passphrase protection state of the key. The dialog is modal, so
* it blocks the caller until it is closed, and all checks and
* interactions must happen in the callback. The dialog is closed after
* the callback returns.
*
* @param {string} keyId - The ID of a personal key.
* @param {function(Window):Promise} checks - Receives the dialog
* window.
*/
async function withKeyDetails(keyId, checks) {
const onDialogLoaded = async function (dialogWindow) {
// The dialog sets this while it is still loading, after it has
// determined how the key is protected.
await TestUtils.waitForCondition(
() =>
dialogWindow.document
.getElementById("passphraseStatus")
.getAttribute("data-l10n-id"),
"the passphrase protection state should have been detected"
);
await checks(dialogWindow);
dialogWindow.close();
};
const dialogPromise = BrowserTestUtils.promiseAlertDialog(
null,
KEY_DETAILS_URL,
{ callback: onDialogLoaded }
);
// Returns only after the dialog has been closed.
await EnigmailWindows.openKeyDetails(window, keyId, false);
await dialogPromise;
}
/**
* Select the passphrase protection tab, so that its buttons can be
* clicked.
*
* @param {Window} dialogWindow - The key details dialog.
*/
function selectPassphraseTab(dialogWindow) {
const doc = dialogWindow.document;
doc.getElementById("mainTabs").selectedTab =
doc.getElementById("passphraseTab");
}
function l10nId(dialogWindow, elementId) {
return dialogWindow.document
.getElementById(elementId)
.getAttribute("data-l10n-id");
}
/**
* Click a button of the passphrase tab, and answer the passphrase
* prompt that it triggers.
*
* @param {Window} dialogWindow - The key details dialog.
* @param {string} buttonId - The button to click.
* @param {?string} passphrase - The passphrase to enter, or null to
* cancel the prompt.
*/
async function clickAndAnswerPrompt(dialogWindow, buttonId, passphrase) {
const promptPromise = BrowserTestUtils.promiseAlertDialogOpen();
EventUtils.synthesizeMouseAtCenter(
dialogWindow.document.getElementById(buttonId),
{},
dialogWindow
);
const promptWindow = await promptPromise;
const promptDialog = promptWindow.document.querySelector("dialog");
if (passphrase == null) {
promptDialog.getButton("cancel").click();
return;
}
promptWindow.document.getElementById("password1Textbox").value = passphrase;
promptDialog.getButton("accept").click();
}
/**
* With the pref disabled, a key that uses the automatic protection has
* nothing to manage, so the tab isn't shown.
*/
add_task(async function testAutoProtectedKeyWithPrefDisabled() {
await importAutoProtectedKey();
await withKeyDetails(ALICE_KEY_ID, async dialogWindow => {
const doc = dialogWindow.document;
Assert.ok(
doc.getElementById("passphraseTab").hidden,
"the passphrase tab should be hidden"
);
Assert.ok(
doc.getElementById("passphrasePanel").hidden,
"the passphrase panel should be hidden"
);
Assert.ok(
!doc.getElementById("changeExpiryButton").disabled,
"changing the expiration date should be offered, the primary secret key material is available"
);
});
await OpenPGPTestUtils.removeKeyById(ALICE_KEY_ID, true);
});
/**
* With the pref disabled, a key that is protected by a user-chosen
* passphrase must still offer the tab, otherwise the user has no way to
* return the key to the automatic protection.
*/
add_task(async function testPassphraseProtectedKeyWithPrefDisabled() {
await importPassphraseProtectedKey();
await withKeyDetails(ALICE_KEY_ID, async dialogWindow => {
const doc = dialogWindow.document;
Assert.ok(
!doc.getElementById("passphraseTab").hidden,
"the passphrase tab should be shown for a key that is protected by a passphrase"
);
Assert.equal(
l10nId(dialogWindow, "passphraseStatus"),
"openpgp-passphrase-status-user-passphrase",
"the key should be reported as protected by a passphrase"
);
Assert.ok(
doc.getElementById("passphraseInstruction").hidden,
"the instruction should be hidden"
);
Assert.ok(
doc.getElementById("lockBox").hidden,
"setting a passphrase should not be offered while the pref is disabled"
);
Assert.ok(
doc.getElementById("unlockBox").hidden,
"unlocking should not be offered separately, it happens on demand"
);
Assert.ok(
!doc.getElementById("removeProtection").hidden,
"removing the passphrase protection should be offered"
);
Assert.ok(
doc.getElementById("usePrimaryPassword").hidden,
"no primary password is set, so that alternative should be hidden"
);
// Use the escape hatch, and unlock the key on demand while doing so.
selectPassphraseTab(dialogWindow);
await clickAndAnswerPrompt(
dialogWindow,
"removeProtection",
ALICE_PASSPHRASE
);
await TestUtils.waitForCondition(
() =>
l10nId(dialogWindow, "passphraseStatus") ==
"openpgp-passphrase-status-unprotected",
"the key should be reported as using the automatic protection"
);
Assert.ok(
doc.getElementById("removeProtection").hidden,
"removing the passphrase protection should no longer be offered"
);
});
// The change must be persistent, so the tab is no longer needed.
await withKeyDetails(ALICE_KEY_ID, async dialogWindow => {
Assert.equal(
l10nId(dialogWindow, "passphraseStatus"),
"openpgp-passphrase-status-unprotected",
"the key should still use the automatic protection"
);
Assert.ok(
dialogWindow.document.getElementById("passphraseTab").hidden,
"the passphrase tab should be hidden again"
);
});
await OpenPGPTestUtils.removeKeyById(ALICE_KEY_ID, true);
});
/**
* With the pref enabled, a key that uses the automatic protection is
* already unlocked, and a passphrase can be set for it.
*/
add_task(async function testAutoProtectedKeyWithPrefEnabled() {
await SpecialPowers.pushPrefEnv({ set: [[PASSPHRASE_PREF, true]] });
await importAutoProtectedKey();
await withKeyDetails(ALICE_KEY_ID, async dialogWindow => {
const doc = dialogWindow.document;
Assert.ok(
!doc.getElementById("passphraseTab").hidden,
"the passphrase tab should be shown"
);
Assert.equal(
l10nId(dialogWindow, "passphraseStatus"),
"openpgp-passphrase-status-unprotected",
"the key should be reported as using the automatic protection"
);
Assert.ok(
!doc.getElementById("lockBox").hidden,
"setting a passphrase should be offered"
);
Assert.ok(
doc.getElementById("unlockBox").hidden,
"unlocking should not be offered, the key is already unlocked"
);
});
await OpenPGPTestUtils.removeKeyById(ALICE_KEY_ID, true);
await SpecialPowers.popPrefEnv();
});
/**
* A failed unlock must be reported, because a key pair consists of
* several keys, and earlier keys may have been unlocked already.
*/
add_task(async function testUnlockFailureIsReported() {
await SpecialPowers.pushPrefEnv({ set: [[PASSPHRASE_PREF, true]] });
await importPassphraseProtectedKey();
await withKeyDetails(ALICE_KEY_ID, async dialogWindow => {
const doc = dialogWindow.document;
Assert.ok(
!doc.getElementById("unlockBox").hidden,
"unlocking should be offered"
);
selectPassphraseTab(dialogWindow);
await clickAndAnswerPrompt(dialogWindow, "unlock", null);
await TestUtils.waitForCondition(
() =>
l10nId(dialogWindow, "passphraseInstruction") ==
"openpgp-passphrase-unlock-failed",
"the failed unlock should be reported"
);
Assert.ok(
!doc.getElementById("passphraseInstruction").hidden,
"the report should be visible"
);
Assert.ok(
!doc.getElementById("unlockBox").hidden,
"unlocking should still be offered"
);
await clickAndAnswerPrompt(dialogWindow, "unlock", ALICE_PASSPHRASE);
await TestUtils.waitForCondition(
() =>
l10nId(dialogWindow, "passphraseInstruction") ==
"openpgp-passphrase-unlocked",
"the successful unlock should be reported"
);
Assert.ok(
doc.getElementById("unlockBox").hidden,
"unlocking should no longer be offered"
);
Assert.ok(
!doc.getElementById("lockBox").hidden,
"changing the passphrase should be offered"
);
});
await OpenPGPTestUtils.removeKeyById(ALICE_KEY_ID, true);
await SpecialPowers.popPrefEnv();
});
/**
* Changing the expiration date requires a new signature by the primary
* key, which is impossible if its secret key material is missing.
*/
add_task(async function testOfflinePrimaryKey() {
await OpenPGPTestUtils.importPrivateKey(
window,
keyFile("ofelia-secret-subkeys.asc")
);
await withKeyDetails(OFELIA_KEY_ID, async dialogWindow => {
Assert.ok(
dialogWindow.document.getElementById("changeExpiryButton").disabled,
"changing the expiration date must not be offered without the primary secret key material"
);
Assert.equal(
l10nId(dialogWindow, "passphraseStatus"),
"openpgp-passphrase-status-unprotected",
"the subkeys should be reported as using the automatic protection"
);
});
await OpenPGPTestUtils.removeKeyById(OFELIA_KEY_ID, true);
});