summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/ui/keyWizard.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp/content/ui/keyWizard.js')
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyWizard.js1195
1 files changed, 1195 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/ui/keyWizard.js b/comm/mail/extensions/openpgp/content/ui/keyWizard.js
new file mode 100644
index 0000000000..fe699bcc7e
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyWizard.js
@@ -0,0 +1,1195 @@
+/* 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/. */
+
+"use strict";
+
+/* global GetEnigmailSvc */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { OpenPGPMasterpass } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/masterpass.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+});
+
+// UI variables.
+var gIdentity;
+var gIdentityList;
+var gSubDialog;
+var kStartSection;
+var kDialog;
+var kCurrentSection = "start";
+var kGenerating = false;
+var kButtonLabel;
+
+// OpenPGP variables.
+var gKeygenRequest;
+var gAllData = "";
+var gGeneratedKey = null;
+var gFiles;
+
+const DEFAULT_FILE_PERMS = 0o600;
+
+// The revocation strings are not localization since the revocation certificate
+// will be published to others who may not know the native language of the user.
+const revocationFilePrefix1 =
+ "This is a revocation certificate for the OpenPGP key:";
+const revocationFilePrefix2 = `
+A revocation certificate is kind of a "kill switch" to publicly
+declare that a key shall no longer be used. It is not possible
+to retract such a revocation certificate once it has been published.
+
+Use it to revoke this key in case of a secret key compromise, or loss of
+the secret key, or loss of passphrase of the secret key.
+
+To avoid an accidental use of this file, a colon has been inserted
+before the 5 dashes below. Remove this colon with a text editor
+before importing and publishing this revocation certificate.
+
+:`;
+
+var syncl10n = new Localization(["messenger/openpgp/keyWizard.ftl"], true);
+
+// Dialog event listeners.
+document.addEventListener("dialogaccept", wizardContinue);
+document.addEventListener("dialogextra1", goBack);
+document.addEventListener("dialogcancel", onClose);
+
+/**
+ * Initialize the keyWizard dialog.
+ */
+async function init() {
+ gSubDialog = window.arguments[0].gSubDialog;
+ gIdentity = window.arguments[0].identity || null;
+ gIdentityList = document.getElementById("userIdentity");
+
+ kStartSection = document.getElementById("wizardStart");
+ kDialog = document.querySelector("dialog");
+
+ await initIdentity();
+
+ // Show the GnuPG radio selection if the pref is enabled.
+ if (Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg")) {
+ document.getElementById("externalOpenPgp").removeAttribute("hidden");
+ }
+
+ // After the dialog is visible, disable the event listeners causing it to
+ // close when clicking on the overlay or hitting the Esc key, and remove the
+ // close button from the header. This is necessary to control the escape
+ // point and prevent the accidental dismiss of the dialog during important
+ // processes, like the generation or importing of a key.
+ setTimeout(() => {
+ // Check if the attribute is not null. This can be removed after the full
+ // conversion of the Key Manager into a SubDialog in Bug 1652537.
+ if (gSubDialog) {
+ gSubDialog._topDialog._removeDialogEventListeners();
+ gSubDialog._topDialog._closeButton.remove();
+ resizeDialog();
+ }
+ }, 150);
+
+ // Switch directly to the create screen if requested by the user.
+ if (window.arguments[0].isCreate) {
+ document.getElementById("openPgpKeyChoices").value = 0;
+
+ switchSection();
+ }
+
+ // Switch directly to the import screen if requested by the user.
+ if (window.arguments[0].isImport) {
+ document.getElementById("openPgpKeyChoices").value = 1;
+
+ // Disable the "Continue" button so the user can't accidentally click on it.
+ // See bug 1689980.
+ kDialog.getButton("accept").setAttribute("disabled", true);
+
+ switchSection();
+ }
+}
+
+function onProtectionChange() {
+ let pw1Element = document.getElementById("passwordInput");
+ let pw2Element = document.getElementById("passwordConfirm");
+
+ let pw1 = pw1Element.value;
+ let pw2 = pw2Element.value;
+
+ let inputDisabled = document.getElementById("keygenAutoProtection").selected;
+ pw1Element.disabled = inputDisabled;
+ pw2Element.disabled = inputDisabled;
+
+ let buttonEnabled = inputDisabled || (!inputDisabled && pw1 == pw2 && pw1);
+ let ok = kDialog.getButton("accept");
+ ok.disabled = !buttonEnabled;
+}
+
+/**
+ * Populate the identity menulist with all the valid and available identities
+ * and autoselect the current identity if available.
+ */
+async function initIdentity() {
+ let identityListPopup = document.getElementById("userIdentityPopup");
+
+ for (let identity of MailServices.accounts.allIdentities) {
+ // Skip invalid and non-email identities.
+ if (!identity.valid || !identity.email) {
+ continue;
+ }
+
+ // Interrupt if no server was defined for this identity.
+ let servers = MailServices.accounts.getServersForIdentity(identity);
+ if (servers.length == 0) {
+ continue;
+ }
+
+ let item = document.createXULElement("menuitem");
+ item.setAttribute(
+ "label",
+ `${identity.identityName} - ${servers[0].prettyName}`
+ );
+ item.setAttribute("class", "identity-popup-item");
+ item.setAttribute("accountname", servers[0].prettyName);
+ item.setAttribute("identitykey", identity.key);
+ item.setAttribute("email", identity.email);
+
+ identityListPopup.appendChild(item);
+
+ if (gIdentity && gIdentity.key == identity.key) {
+ gIdentityList.selectedItem = item;
+ }
+ }
+
+ // If not identity was originally passed during the creation of this dialog,
+ // select the first available value.
+ if (!gIdentity) {
+ gIdentityList.selectedIndex = 0;
+ }
+
+ await setIdentity();
+}
+
+/**
+ * Update the currently used identity to reflect the user selection from the
+ * identity menulist.
+ */
+async function setIdentity() {
+ if (gIdentityList.selectedItem) {
+ gIdentity = MailServices.accounts.getIdentity(
+ gIdentityList.selectedItem.getAttribute("identitykey")
+ );
+
+ document.l10n.setAttributes(
+ document.documentElement,
+ "key-wizard-dialog-window",
+ {
+ identity: gIdentity.email,
+ }
+ );
+ }
+}
+
+/**
+ * Intercept the dialogaccept command to implement a wizard like setup workflow.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function wizardContinue(event) {
+ event.preventDefault();
+
+ // Pretty impossible scenario but just in case if no radio button is
+ // currently selected, bail out.
+ if (!document.getElementById("openPgpKeyChoices").value) {
+ return;
+ }
+
+ // Trigger an action based on the currently visible section.
+ if (kCurrentSection != "start") {
+ wizardNextStep();
+ return;
+ }
+
+ // Disable the `Continue` button.
+ kDialog.getButton("accept").setAttribute("disabled", true);
+
+ kStartSection.addEventListener("transitionend", switchSection, {
+ once: true,
+ });
+ kStartSection.classList.add("hide");
+}
+
+/**
+ * Separated method dealing with the section switching to allow the removal of
+ * the event listener to prevent stacking.
+ */
+function switchSection() {
+ kStartSection.setAttribute("hidden", true);
+
+ // Save the current label of the accept button in order to restore it later.
+ kButtonLabel = kDialog.getButton("accept").label;
+
+ // Update the UI based on the radiogroup selection.
+ switch (document.getElementById("openPgpKeyChoices").value) {
+ case "0":
+ wizardCreateKey();
+ break;
+
+ case "1":
+ wizardImportKey();
+ break;
+
+ case "2":
+ wizardExternalKey();
+ break;
+ }
+
+ // Show the `Go back` button.
+ kDialog.getButton("extra1").hidden = false;
+ resizeDialog();
+}
+
+/**
+ * Handle the next step of the wizard based on the currently visible section.
+ */
+async function wizardNextStep() {
+ switch (kCurrentSection) {
+ case "create":
+ await openPgpKeygenStart();
+ break;
+
+ case "import":
+ await openPgpImportStart();
+ break;
+
+ case "importComplete":
+ openPgpImportComplete();
+ break;
+
+ case "external":
+ openPgpExternalComplete();
+ break;
+ }
+}
+
+/**
+ * Go back to the initial view of the wizard.
+ */
+function goBack() {
+ let section = document.querySelector(".wizard-section:not([hidden])");
+ section.addEventListener("transitionend", backToStart, { once: true });
+ section.classList.add("hide-reverse");
+}
+
+/**
+ * Hide the currently visible section at the end of the animation, remove the
+ * listener to prevent stacking, and trigger the reveal of the first section.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function backToStart(event) {
+ // Hide the `Go Back` button.
+ kDialog.getButton("extra1").hidden = true;
+
+ // Enable the `Continue` button.
+ kDialog.getButton("accept").removeAttribute("disabled");
+
+ kDialog.getButton("accept").label = kButtonLabel;
+ kDialog.getButton("accept").classList.remove("primary");
+
+ // Reset the import section.
+ clearImportWarningNotifications();
+ document.getElementById("importKeyIntro").hidden = false;
+ document.getElementById("importKeyListContainer").collapsed = true;
+
+ event.target.setAttribute("hidden", true);
+
+ // Reset section key.
+ kCurrentSection = "start";
+
+ revealSection("wizardStart");
+}
+
+/**
+ * Create a new inline notification to append to the import warning container.
+ *
+ * @returns {XULElement} - The description element inside the notification.
+ */
+async function addImportWarningNotification() {
+ let notification = document.createXULElement("hbox");
+ notification.classList.add(
+ "inline-notification-container",
+ "error-container"
+ );
+
+ let wrapper = document.createXULElement("hbox");
+ wrapper.classList.add("inline-notification-wrapper", "align-center");
+
+ let image = document.createElement("img");
+ image.classList.add("notification-image");
+ image.setAttribute("src", "chrome://global/skin/icons/warning.svg");
+ image.setAttribute("alt", "");
+
+ let description = document.createXULElement("description");
+
+ wrapper.appendChild(image);
+ wrapper.appendChild(description);
+
+ notification.appendChild(wrapper);
+
+ let container = document.getElementById("openPgpImportWarning");
+ container.appendChild(notification);
+
+ // Show the notification container.
+ container.removeAttribute("hidden");
+
+ return description;
+}
+
+/**
+ * Remove all inline errors from the notification area of the import section.
+ */
+function clearImportWarningNotifications() {
+ let container = document.getElementById("openPgpImportWarning");
+
+ // Remove any existing notification.
+ for (let notification of container.querySelectorAll(
+ ".inline-notification-container"
+ )) {
+ notification.remove();
+ }
+
+ // Hide the entire notification container.
+ container.hidden = true;
+}
+
+/**
+ * Show the Key Creation section.
+ */
+async function wizardCreateKey() {
+ kCurrentSection = "create";
+ revealSection("wizardCreateKey");
+
+ kDialog.getButton("accept").label = await document.l10n.formatValue(
+ "openpgp-keygen-button"
+ );
+ kDialog.getButton("accept").classList.add("primary");
+
+ if (!gIdentity.fullName) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-long-expiry"
+ );
+ return;
+ }
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ document.getElementById("keygenPassphraseSection").hidden =
+ !sepPassphraseEnabled;
+
+ if (sepPassphraseEnabled) {
+ let usingPP = LoginHelper.isPrimaryPasswordSet();
+ let autoProt = document.getElementById("keygenAutoProtection");
+
+ document.l10n.setAttributes(
+ autoProt,
+ usingPP
+ ? "radio-keygen-protect-primary-pass"
+ : "radio-keygen-no-protection"
+ );
+
+ autoProt.setAttribute("selected", true);
+ document
+ .getElementById("keygenPassphraseProtection")
+ .removeAttribute("selected");
+ }
+
+ // This also handles enable/disabling the accept/ok button.
+ onProtectionChange();
+}
+
+/**
+ * Show the Key Import section.
+ */
+function wizardImportKey() {
+ kCurrentSection = "import";
+ revealSection("wizardImportKey");
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ let keepPassphrasesItem = document.getElementById(
+ "openPgpKeygenKeepPassphrases"
+ );
+ keepPassphrasesItem.hidden = !sepPassphraseEnabled;
+ keepPassphrasesItem.checked = false;
+}
+
+/**
+ * Show the Key Setup via external smartcard section.
+ */
+async function wizardExternalKey() {
+ kCurrentSection = "external";
+ revealSection("wizardExternalKey");
+
+ kDialog.getButton("accept").label = await document.l10n.formatValue(
+ "openpgp-save-external-button"
+ );
+ kDialog.getButton("accept").classList.add("primary");
+
+ // If the user is already using an external GnuPG key, populate the input,
+ // show the warning description, and enable the primary button.
+ if (gIdentity.getBoolAttribute("is_gnupg_key_id")) {
+ document.getElementById("externalKey").value =
+ gIdentity.getUnicharAttribute("last_entered_external_gnupg_key_id");
+ document.getElementById("openPgpExternalWarning").collapsed = false;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ } else {
+ document.getElementById("openPgpExternalWarning").collapsed = true;
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ }
+}
+
+/**
+ * Animate the reveal of a section of the wizard.
+ *
+ * @param {string} id - The id of the section to reveal.
+ */
+function revealSection(id) {
+ let section = document.getElementById(id);
+ section.removeAttribute("hidden");
+
+ // Timeout to animate after the hidden attribute has been removed.
+ setTimeout(() => {
+ section.classList.remove("hide", "hide-reverse");
+ });
+
+ resizeDialog();
+}
+
+/**
+ * Enable or disable the elements based on the radiogroup selection.
+ *
+ * @param {Event} event - The DOM event triggered on change.
+ */
+function onExpirationChange(event) {
+ document
+ .getElementById("expireInput")
+ .toggleAttribute("disabled", event.target.value != 0);
+ document.getElementById("timeScale").disabled = event.target.value != 0;
+
+ validateExpiration();
+}
+
+/**
+ * Enable or disable the #keySize input field based on the current selection of
+ * the #keyType radio group.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function onKeyTypeChange(event) {
+ document.getElementById("keySize").disabled = event.target.value == "ECC";
+}
+
+/**
+ * Intercept the cancel event to prevent accidental closing if the generation of
+ * a key is currently in progress.
+ *
+ * @param {Event} event - The DOM event.
+ */
+function onClose(event) {
+ if (kGenerating) {
+ event.preventDefault();
+ }
+
+ window.arguments[0].cancelCallback();
+}
+
+/**
+ * Validate the expiration time of a newly generated key when the user changes
+ * values. Disable the "Generate Key" button and show an alert if the selected
+ * value is less than 1 day or more than 100 years.
+ */
+async function validateExpiration() {
+ // If the key doesn't have an expiration date, hide the warning message and
+ // enable the "Generate Key" button.
+ if (document.getElementById("openPgpKeygeExpiry").value == 1) {
+ document.getElementById("openPgpWarning").collapsed = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ return;
+ }
+
+ // Calculate the selected expiration date.
+ let expiryTime =
+ Number(document.getElementById("expireInput").value) *
+ Number(document.getElementById("timeScale").value);
+
+ // If the expiration date exceeds 100 years.
+ if (expiryTime > 36500) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-long-expiry"
+ );
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ resizeDialog();
+ return;
+ }
+
+ // If the expiration date is shorter than 1 day.
+ if (expiryTime <= 0) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-short-expiry"
+ );
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ resizeDialog();
+ return;
+ }
+
+ // If the previous conditions are false, hide the warning message and
+ // enable the "Generate Key" button since the expiration date is valid.
+ document.getElementById("openPgpWarning").collapsed = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+}
+
+/**
+ * Resize the dialog to account for the newly visible sections.
+ */
+function resizeDialog() {
+ // Check if the attribute is not null. This can be removed after the full
+ // conversion of the Key Manager into a SubDialog in Bug 1652537.
+ if (gSubDialog && gSubDialog._topDialog) {
+ gSubDialog._topDialog.resizeVertically();
+ } else {
+ window.sizeToContent();
+ }
+}
+
+/**
+ * Start the generation of a new OpenPGP Key.
+ */
+async function openPgpKeygenStart() {
+ let openPgpWarning = document.getElementById("openPgpWarning");
+ let openPgpWarningText = document.getElementById("openPgpWarningDescription");
+ openPgpWarning.collapsed = true;
+
+ // If a key generation request is already pending, warn the user and
+ // don't proceed.
+ if (gKeygenRequest) {
+ let req = gKeygenRequest.QueryInterface(Ci.nsIRequest);
+
+ if (req.isPending()) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(openPgpWarningText, "openpgp-keygen-ongoing");
+ return;
+ }
+ }
+
+ // Reset global variables to be sure.
+ gGeneratedKey = null;
+ gAllData = "";
+
+ let enigmailSvc = GetEnigmailSvc();
+ if (!enigmailSvc) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-core"
+ );
+ closeOverlay();
+
+ throw new Error("GetEnigmailSvc failed");
+ }
+
+ // Show wizard overlay before the start of the generation process. This is
+ // necessary because the generation happens synchronously and blocks the UI.
+ // We need to show the overlay before it, otherwise it would flash and freeze.
+ // This should be moved after the Services.prompt.confirmEx() method
+ // once Bug 1617444 is implemented.
+ let overlay = document.getElementById("wizardOverlay");
+ overlay.removeAttribute("hidden");
+ overlay.classList.remove("hide");
+
+ // Ask for confirmation before triggering the generation of a new key.
+ document.l10n.setAttributes(
+ document.getElementById("wizardOverlayQuestion"),
+ "openpgp-key-confirm",
+ {
+ identity: `${gIdentity.fullName} <b>"${gIdentity.email}"</b>`,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("wizardOverlayTitle"),
+ "openpgp-keygen-progress-title"
+ );
+}
+
+async function openPgpKeygenConfirm() {
+ document.getElementById("openPgpKeygenConfirm").collapsed = true;
+ document.getElementById("openPgpKeygenProcess").removeAttribute("collapsed");
+
+ let openPgpWarning = document.getElementById("openPgpWarning");
+ let openPgpWarningText = document.getElementById("openPgpWarningDescription");
+ openPgpWarning.collapsed = true;
+
+ kGenerating = true;
+
+ let password;
+ let cApi = EnigmailCryptoAPI();
+ let newId = null;
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+
+ if (
+ !sepPassphraseEnabled ||
+ document.getElementById("keygenAutoProtection").selected
+ ) {
+ password = await OpenPGPMasterpass.retrieveOpenPGPPassword();
+ } else {
+ password = document.getElementById("passwordInput").value;
+ }
+ newId = await cApi.genKey(
+ `${gIdentity.fullName} <${gIdentity.email}>`,
+ document.getElementById("keyType").value,
+ Number(document.getElementById("keySize").value),
+ document.getElementById("openPgpKeygeExpiry").value == 1
+ ? 0
+ : Number(document.getElementById("expireInput").value) *
+ Number(document.getElementById("timeScale").value),
+ password
+ );
+
+ gGeneratedKey = newId;
+
+ EnigmailWindows.keyManReloadKeys();
+
+ gKeygenRequest = null;
+ kGenerating = false;
+
+ // For wathever reason, the key wasn't generated. Show an error message and
+ // hide the processing overlay.
+ if (!gGeneratedKey) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-failed"
+ );
+ closeOverlay();
+
+ throw new Error("key generation failed");
+ }
+
+ console.debug("saving new key id " + gGeneratedKey);
+ Services.prefs.savePrefFile(null);
+
+ // Hide wizard overlay at the end of the generation process.
+ closeOverlay();
+ EnigmailKeyRing.clearCache();
+
+ let rev = await cApi.unlockAndGetNewRevocation(
+ `0x${gGeneratedKey}`,
+ password
+ );
+ if (!rev) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-revocation",
+ {
+ key: gGeneratedKey,
+ }
+ );
+ closeOverlay();
+
+ throw new Error("failed to obtain revocation for key " + gGeneratedKey);
+ }
+
+ let revFull =
+ revocationFilePrefix1 +
+ "\n\n" +
+ gGeneratedKey +
+ "\n" +
+ revocationFilePrefix2 +
+ rev;
+
+ let revFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ revFile.append(`0x${gGeneratedKey}_rev.asc`);
+
+ // Create a revokation cert in the Thunderbird profile directory.
+ await IOUtils.writeUTF8(revFile.path, revFull);
+
+ // Key successfully created. Close the dialog and show a confirmation message.
+ // Assigning the key to an identity is the responsibility of the caller,
+ // so we pass back what we created.
+ window.arguments[0].okCallback(gGeneratedKey);
+ window.close();
+}
+
+/**
+ * Cancel the keygen process, ask for confirmation before proceeding.
+ */
+async function openPgpKeygenCancel() {
+ let [abortTitle, abortText] = await document.l10n.formatValues([
+ { id: "openpgp-keygen-abort-title" },
+ { id: "openpgp-keygen-abort" },
+ ]);
+
+ if (
+ kGenerating &&
+ Services.prompt.confirmEx(
+ window,
+ abortTitle,
+ abortText,
+ Services.prompt.STD_YES_NO_BUTTONS,
+ "",
+ "",
+ "",
+ "",
+ {}
+ ) != 0
+ ) {
+ return;
+ }
+
+ closeOverlay();
+ gKeygenRequest.kill(false);
+ kGenerating = false;
+}
+
+/**
+ * Close the processing wizard overlay.
+ */
+function closeOverlay() {
+ document.getElementById("openPgpKeygenConfirm").removeAttribute("collapsed");
+ document.getElementById("openPgpKeygenProcess").collapsed = true;
+
+ let overlay = document.getElementById("wizardOverlay");
+
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+}
+
+/**
+ * Add the "hidden" attribute tot he processing wizard overlay after the CSS
+ * transition ended.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function hideOverlay(event) {
+ event.target.setAttribute("hidden", true);
+ resizeDialog();
+}
+
+async function importSecretKey() {
+ let [importTitle, importType] = await document.l10n.formatValues([
+ { id: "import-key-file" },
+ { id: "gnupg-file" },
+ ]);
+
+ // Reset the array of selected files.
+ gFiles = [];
+
+ let files = EnigmailDialog.filePicker(
+ window,
+ importTitle,
+ "",
+ false,
+ true,
+ "*.asc",
+ "",
+ [importType, "*.asc;*.gpg;*.pgp"]
+ );
+
+ if (!files.length) {
+ return;
+ }
+
+ // Clear and hide the warning notification section.
+ clearImportWarningNotifications();
+
+ // Clear the key list from any previously listed key.
+ let keyList = document.getElementById("importKeyList");
+ while (keyList.lastChild) {
+ keyList.lastChild.remove();
+ }
+
+ let keyCount = 0;
+ for (let file of files) {
+ // Skip the file and show a warning message if larger than 5MB.
+ if (file.fileSize > 5000000) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "import-error-file-size"
+ );
+ continue;
+ }
+
+ let errorMsgObj = {};
+ // Fetch the list of all the available keys inside the selected file.
+ let importKeys = await EnigmailKey.getKeyListFromKeyFile(
+ file,
+ errorMsgObj,
+ false,
+ true
+ );
+
+ // Skip the file and show a warning message if the import failed.
+ if (!importKeys || !importKeys.length || errorMsgObj.value) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "import-error-failed",
+ {
+ error: errorMsgObj.value,
+ }
+ );
+ continue;
+ }
+
+ await appendFetchedKeys(importKeys);
+ keyCount += importKeys.length;
+
+ // Add the current file to the list of valid files to import.
+ gFiles.push(file);
+ }
+
+ // Update the list count recap and show the container.
+ document.l10n.setAttributes(
+ document.getElementById("keyListCount"),
+ "openpgp-import-key-list-amount-2",
+ {
+ count: keyCount,
+ }
+ );
+
+ document.getElementById("importKeyListContainer").collapsed = !keyCount;
+
+ // Hide the intro section and enable the import of keys only if we have valid
+ // keys currently listed.
+ if (keyCount) {
+ document.getElementById("importKeyIntro").hidden = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ kDialog.getButton("accept").classList.add("primary");
+ }
+
+ resizeDialog();
+}
+
+/**
+ * Populate the key list in the import dialog with all the valid keys fetched
+ * from a single file.
+ *
+ * @param {string[]} importKeys - The array of keys fetched from a single file.
+ */
+async function appendFetchedKeys(importKeys) {
+ let keyList = document.getElementById("importKeyList");
+
+ // List all the keys fetched from the file.
+ for (let key of importKeys) {
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row", "selected");
+
+ let titleContainer = document.createXULElement("vbox");
+
+ let id = document.createXULElement("label");
+ id.classList.add("openpgp-key-id");
+ id.value = `0x${key.id}`;
+
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.value = key.name;
+
+ titleContainer.appendChild(id);
+ titleContainer.appendChild(name);
+
+ // Allow users to treat imported keys as "Personal".
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.setAttribute("id", `${key.id}-set-personal`);
+ document.l10n.setAttributes(checkbox, "import-key-personal-checkbox");
+ checkbox.checked = true;
+
+ container.appendChild(titleContainer);
+ container.appendChild(checkbox);
+
+ keyList.appendChild(container);
+ }
+}
+
+async function openPgpImportStart() {
+ if (!gFiles.length) {
+ return;
+ }
+
+ kGenerating = true;
+
+ // Show the overlay.
+ let overlay = document.getElementById("wizardImportOverlay");
+ overlay.removeAttribute("hidden");
+ overlay.classList.remove("hide");
+
+ // Clear and hide the warning notification section.
+ clearImportWarningNotifications();
+
+ // Clear the list of any previously improted keys from the DOM.
+ let keyList = document.getElementById("importKeyListRecap");
+ while (keyList.lastChild) {
+ keyList.lastChild.remove();
+ }
+
+ let keyCount = 0;
+ for (let file of gFiles) {
+ let resultKeys = {};
+ let errorMsgObj = {};
+
+ // keepPassphrases false is the classic behavior.
+ let keepPassphrases = false;
+
+ // If the pref is on, we allow the user to decide what to do.
+ let allowSeparatePassphrases = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ if (allowSeparatePassphrases) {
+ keepPassphrases = document.getElementById(
+ "openPgpKeygenKeepPassphrases"
+ ).checked;
+ }
+
+ let exitCode = await EnigmailKeyRing.importSecKeyFromFile(
+ window,
+ passphrasePromptCallback,
+ keepPassphrases,
+ file,
+ errorMsgObj,
+ resultKeys
+ );
+
+ // Skip this file if something went wrong.
+ if (exitCode !== 0) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "openpgp-import-keys-failed",
+ {
+ error: errorMsgObj.value,
+ }
+ );
+ continue;
+ }
+
+ await appendImportedKeys(resultKeys);
+ keyCount += resultKeys.keys.length;
+ }
+
+ // Hide the previous key list container and title.
+ document.getElementById("importKeyListContainer").collapsed = keyCount;
+ document.getElementById("importKeyTitle").hidden = keyCount;
+
+ // Show the successful final screen only if at least one key was imported.
+ if (keyCount) {
+ // Update the dialog buttons for the final stage.
+ kDialog.getButton("extra1").hidden = true;
+ kDialog.getButton("cancel").hidden = true;
+
+ // Update the `Continue` button.
+ document.l10n.setAttributes(
+ kDialog.getButton("accept"),
+ "openpgp-keygen-import-complete"
+ );
+ kCurrentSection = "importComplete";
+
+ // Show the recently built key list.
+ document.getElementById("importKeyListSuccess").collapsed = false;
+ }
+
+ // Hide the loading overlay.
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+
+ resizeDialog();
+ kGenerating = false;
+}
+
+/**
+ * Populate the key list in the import dialog with all the valid keys imported
+ * from a single file.
+ *
+ * @param {string[]} resultKeys - The array of keys imported from a single file.
+ */
+async function appendImportedKeys(resultKeys) {
+ let keyList = document.getElementById("importKeyListRecap");
+
+ for (let keyId of resultKeys.keys) {
+ if (keyId.search(/^0x/) === 0) {
+ keyId = keyId.substr(2).toUpperCase();
+ }
+
+ let key = EnigmailKeyRing.getKeyById(keyId);
+
+ if (key && key.fpr) {
+ // If the checkbox was checked, update the acceptance of the key.
+ if (document.getElementById(`${key.keyId}-set-personal`).checked) {
+ PgpSqliteDb2.acceptAsPersonalKey(key.fpr);
+ }
+
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row");
+
+ // Start key info section.
+ let grid = document.createXULElement("hbox");
+ grid.classList.add("extra-information-label");
+
+ // Key identity.
+ let identityLabel = document.createXULElement("label");
+ identityLabel.classList.add("extra-information-label-type");
+ document.l10n.setAttributes(
+ identityLabel,
+ "openpgp-import-identity-label"
+ );
+
+ let identityValue = document.createXULElement("label");
+ identityValue.value = key.userId;
+
+ grid.appendChild(identityLabel);
+ grid.appendChild(identityValue);
+
+ // Key fingerprint.
+ let fingerprintLabel = document.createXULElement("label");
+ document.l10n.setAttributes(
+ fingerprintLabel,
+ "openpgp-import-fingerprint-label"
+ );
+ fingerprintLabel.classList.add("extra-information-label-type");
+
+ let fingerprintInput = document.createXULElement("label");
+ fingerprintInput.value = EnigmailKey.formatFpr(key.fpr);
+
+ grid.appendChild(fingerprintLabel);
+ grid.appendChild(fingerprintInput);
+
+ // Key creation date.
+ let createdLabel = document.createXULElement("label");
+ document.l10n.setAttributes(createdLabel, "openpgp-import-created-label");
+ createdLabel.classList.add("extra-information-label-type");
+
+ let createdValue = document.createXULElement("label");
+ createdValue.value = key.created;
+
+ grid.appendChild(createdLabel);
+ grid.appendChild(createdValue);
+
+ // Key bits.
+ let bitsLabel = document.createXULElement("label");
+ bitsLabel.classList.add("extra-information-label-type");
+ document.l10n.setAttributes(bitsLabel, "openpgp-import-bits-label");
+
+ let bitsValue = document.createXULElement("label");
+ bitsValue.value = key.keySize;
+
+ grid.appendChild(bitsLabel);
+ grid.appendChild(bitsValue);
+ // End key info section.
+
+ let info = document.createXULElement("button");
+ info.classList.add("openpgp-image-btn", "openpgp-props-btn");
+ document.l10n.setAttributes(info, "openpgp-import-key-props");
+ info.addEventListener("command", () => {
+ window.arguments[0].keyDetailsDialog(key.keyId);
+ });
+
+ container.appendChild(grid);
+ container.appendChild(info);
+
+ keyList.appendChild(container);
+ }
+ }
+}
+
+function openPgpImportComplete() {
+ window.arguments[0].okImportCallback();
+ window.close();
+}
+
+/**
+ * Opens a prompt asking the user to enter the passphrase for a given key id.
+ *
+ * @param {object} win - The current window.
+ * @param {string} keyId - The ID of the imported key.
+ * @param {object} resultFlags - Keep track of the cancelled action.
+ *
+ * @returns {string} - The entered passphrase or empty.
+ */
+function passphrasePromptCallback(win, promptString, resultFlags) {
+ let passphrase = { value: "" };
+
+ // We need to fetch these strings synchronously in order to properly work with
+ // the RNP key import method, which is not async.
+ let title = syncl10n.formatValueSync("openpgp-passphrase-prompt-title");
+
+ let prompt = Services.prompt.promptPassword(
+ win,
+ title,
+ promptString,
+ passphrase,
+ null,
+ {}
+ );
+
+ if (!prompt) {
+ let overlay = document.getElementById("wizardImportOverlay");
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+ kGenerating = false;
+ }
+
+ resultFlags.canceled = !prompt;
+ return !prompt ? "" : passphrase.value;
+}
+
+function toggleSaveButton(event) {
+ kDialog
+ .getButton("accept")
+ .toggleAttribute("disabled", !event.target.value.trim());
+}
+
+/**
+ * Save the GnuPG Key for the current identity and trigger a callback.
+ */
+function openPgpExternalComplete() {
+ gIdentity.setBoolAttribute("is_gnupg_key_id", true);
+
+ let externalKey = document.getElementById("externalKey").value;
+ gIdentity.setUnicharAttribute("openpgp_key_id", externalKey);
+
+ window.arguments[0].okExternalCallback(externalKey);
+ window.close();
+}