Bug 1653300 - Implement the UI to handle OpenPGP External keys. r=KaiE a=wsmwk
authorAlessandro Castellani <alessandro@thunderbird.net>
Sat, 25 Jul 2020 03:15:30 +0200
changeset 38979 5b3c2123f52edfa375b07212486be221365d1bed
parent 38978 9c1f818286592beb906029be866199cd3a7724af
child 38980 797e2c49f29b29603b6577135827d13a349d391f
push id2669
push userkaie@kuix.de
push dateSat, 25 Jul 2020 01:40:31 +0000
treeherdercomm-beta@797e2c49f29b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersKaiE, wsmwk
bugs1653300
Bug 1653300 - Implement the UI to handle OpenPGP External keys. r=KaiE a=wsmwk
mail/extensions/am-e2e/am-e2e.js
mail/extensions/am-e2e/prefs/e2e-prefs.js
mail/extensions/openpgp/content/strings/enigmail.ftl
mail/extensions/openpgp/content/strings/key-wizard.ftl
mail/extensions/openpgp/content/ui/enigmailKeyManager.js
mail/extensions/openpgp/content/ui/keyWizard.js
mail/extensions/openpgp/content/ui/keyWizard.xhtml
mail/themes/shared/mail/accountManage.css
mail/themes/shared/openpgp/keyWizard.css
--- a/mail/extensions/am-e2e/am-e2e.js
+++ b/mail/extensions/am-e2e/am-e2e.js
@@ -596,16 +596,17 @@ function openKeyWizard() {
   }
 
   let args = {
     identity: gIdentity,
     gSubDialog: parent.gSubDialog,
     cancelCallback: reloadOpenPgpUI,
     okCallback: keyWizardSuccess,
     okImportCallback: keyImportSuccess,
+    okExternalCallback: keyExternalSuccess,
     keyDetailsDialog: enigmailKeyDetails,
   };
 
   parent.gSubDialog.open(
     "chrome://openpgp/content/ui/keyWizard.xhtml",
     null,
     args
   );
@@ -626,50 +627,69 @@ async function keyWizardSuccess() {
   // Update the global key with the recently generated key that was assigned to
   // this identity from the Key generation wizard.
   gKeyId = gIdentity.getUnicharAttribute("openpgp_key_id");
 
   reloadOpenPgpUI();
 }
 
 /**
- * Show a succesfull notification after a the import of keys, and trigger the
+ * Show a succesfull notification after an import of keys, and trigger the
  * reload of the key listing UI.
  */
 async function keyImportSuccess() {
   document.l10n.setAttributes(
     document.getElementById("openPgpNotificationDescription"),
     "openpgp-keygen-import-success"
   );
   document.getElementById("openPgpNotification").collapsed = false;
   document.getElementById("openPgpKeyList").collapsed = false;
 
   reloadOpenPgpUI();
 }
 
 /**
+ * Show a succesfull notification after an external key was saved, and trigger
+ * the reload of the key listing UI.
+ */
+async function keyExternalSuccess() {
+  document.l10n.setAttributes(
+    document.getElementById("openPgpNotificationDescription"),
+    "openpgp-keygen-external-success"
+  );
+  document.getElementById("openPgpNotification").collapsed = false;
+  document.getElementById("openPgpKeyList").collapsed = false;
+
+  reloadOpenPgpUI();
+}
+
+/**
  * Collapse the inline notification.
  */
 function closeNotification() {
   document.getElementById("openPgpNotification").collapsed = true;
 }
 
 /**
  * Refresh the UI on init or after a successful OpenPGP Key generation.
  */
 async function reloadOpenPgpUI() {
   let result = {};
   await EnigmailKeyRing.getAllSecretKeysByEmail(gIdentity.email, result, true);
 
   // Show the radiogroup container only if the current identity has keys.
   document.getElementById("openPgpKeyList").collapsed = !result.all.length;
 
+  let externalKey = gIdentity.getUnicharAttribute(
+    "last_entered_external_gnupg_key_id"
+  );
+
   // Interrupt and udpate the UI accordingly if no Key is associated with the
   // current identity.
-  if (!result.all.length) {
+  if (!result.all.length && !externalKey) {
     gKeyId = null;
     updateUIForSelectedOpenPgpKey();
     return;
   }
 
   document.l10n.setAttributes(
     document.getElementById("openPgpDescription"),
     "openpgp-description",
@@ -686,16 +706,62 @@ async function reloadOpenPgpUI() {
     radiogroup.removeChild(radiogroup.lastChild);
   }
 
   // Sort keys by create date from newest to oldest.
   result.all.sort((a, b) => {
     return b.keyCreated - a.keyCreated;
   });
 
+  // If the user has an external Key saved, we show it on top of the list.
+  if (externalKey) {
+    let container = document.createXULElement("vbox");
+    container.id = `openPgpOption${externalKey}`;
+    container.classList.add("content-blocking-category");
+
+    let box = document.createXULElement("hbox");
+
+    let radio = document.createXULElement("radio");
+    radio.setAttribute("flex", "1");
+    radio.id = `openPgp${externalKey}`;
+    radio.value = externalKey;
+    radio.label = `0x${externalKey}`;
+
+    if (externalKey == gIdentity.getUnicharAttribute("openpgp_key_id")) {
+      radio.setAttribute("selected", "true");
+    }
+
+    let remove = document.createXULElement("button");
+    document.l10n.setAttributes(remove, "openpgp-key-remove-external");
+    remove.addEventListener("command", removeExternalKey);
+    remove.classList.add("button-small");
+
+    box.appendChild(radio);
+    box.appendChild(remove);
+
+    let indent = document.createXULElement("vbox");
+    indent.classList.add("indent");
+
+    let dateContainer = document.createXULElement("hbox");
+    dateContainer.classList.add("expiration-date-container");
+    dateContainer.setAttribute("align", "center");
+
+    let external = document.createXULElement("description");
+    external.classList.add("external-pill");
+    document.l10n.setAttributes(external, "key-external-label");
+
+    dateContainer.appendChild(external);
+    indent.appendChild(dateContainer);
+
+    container.appendChild(box);
+    container.appendChild(indent);
+
+    radiogroup.appendChild(container);
+  }
+
   // List all the available keys.
   for (let key of result.all) {
     let container = document.createXULElement("vbox");
     container.id = `openPgpOption${key.keyId}`;
     container.classList.add("content-blocking-category");
 
     let box = document.createXULElement("hbox");
 
@@ -957,17 +1023,17 @@ async function enigmailDeleteKey(key) {
     ]);
 
     Services.prompt.alert(null, alertTitle, alertDescription);
     return;
   }
 
   let l10nKey = key.secretAvailable ? "delete-secret-key" : "delete-pub-key";
   let [title, description] = await document.l10n.formatValues([
-    { id: "delete-key-title", args: { userId: key.userId } },
+    { id: "delete-key-title" },
     { id: l10nKey, args: { userId: key.userId } },
   ]);
 
   // Ask for confirmation before proceeding.
   if (!Services.prompt.confirm(null, title, description)) {
     return;
   }
 
@@ -1068,16 +1134,24 @@ function updateOpenPgpSettings() {
     ""
   );
 
   // Avoid running the method if the key didn't change.
   if (gKeyId == newKey) {
     return;
   }
 
+  // Always update the GnuPG boolean pref to be sure the currently used key is
+  // internal or external.
+  gIdentity.setBoolAttribute(
+    "is_gnupg_key_id",
+    newKey ==
+      gIdentity.getUnicharAttribute("last_entered_external_gnupg_key_id")
+  );
+
   gKeyId = newKey;
 
   if (gKeyId) {
     enableEncryptionControls(true);
     enableSigningControls(true);
   } else {
     let stillHaveOtherEncryption =
       gEncryptionCertName && gEncryptionCertName.value;
@@ -1367,8 +1441,46 @@ async function exportSecretKey(password,
   }
 
   document.l10n.setAttributes(
     document.getElementById("openPgpNotificationDescription"),
     "openpgp-export-secret-success"
   );
   document.getElementById("openPgpNotification").collapsed = false;
 }
+
+/**
+ * Remove the saved external GnuPG Key.
+ */
+async function removeExternalKey() {
+  if (!GetEnigmailSvc()) {
+    return;
+  }
+
+  // Interrupt if the external key is currently being used.
+  if (
+    gIdentity.getUnicharAttribute("last_entered_external_gnupg_key_id") ==
+    gIdentity.getUnicharAttribute("openpgp_key_id")
+  ) {
+    let [alertTitle, alertDescription] = await document.l10n.formatValues([
+      { id: "key-in-use-title" },
+      { id: "delete-key-in-use-description" },
+    ]);
+
+    Services.prompt.alert(null, alertTitle, alertDescription);
+    return;
+  }
+
+  let [title, description] = await document.l10n.formatValues([
+    { id: "delete-external-key-title" },
+    { id: "delete-external-key-description" },
+  ]);
+
+  // Ask for confirmation before proceeding.
+  if (!Services.prompt.confirm(null, title, description)) {
+    return;
+  }
+
+  gIdentity.setBoolAttribute("is_gnupg_key_id", false);
+  gIdentity.setUnicharAttribute("last_entered_external_gnupg_key_id", "");
+
+  reloadOpenPgpUI();
+}
--- a/mail/extensions/am-e2e/prefs/e2e-prefs.js
+++ b/mail/extensions/am-e2e/prefs/e2e-prefs.js
@@ -29,16 +29,18 @@ pref("mail.openpgp.enable", true);
 // If true, we allow the use of GnuPG for OpenPGP secret key operations
 pref("mail.openpgp.allow_external_gnupg", false);
 // If allow_external_gnupg is true: Optionally use a different gpg executable
 pref("mail.openpgp.alternative_gpg_path", "");
 // The hexadecimal OpenPGP key ID used for an identity.
 pref("mail.identity.default.openpgp_key_id", "");
 // If true, then openpgp_key_id is managed externally by GnuPG
 pref("mail.identity.default.is_gnupg_key_id", false);
+// The hexadecimal OpenPGP key ID externally configured by GnuPG used for an identity.
+pref("mail.identity.default.last_entered_external_gnupg_key_id", "");
 
 // When sending, encrypt to this additional key. Not available in release channel builds.
 pref("mail.openpgp.debug.extra_encryption_key", "");
 
 // Hide prefs and menu entries from non-advanced users
 pref("temp.openpgp.advancedUser", false);
 
 // ** enigmail keySel preferences:
--- a/mail/extensions/openpgp/content/strings/enigmail.ftl
+++ b/mail/extensions/openpgp/content/strings/enigmail.ftl
@@ -261,16 +261,18 @@ openpgp-add-key-button =
     .accesskey = A
 
 e2e-learn-more = Learn more
 
 openpgp-keygen-success = OpenPGP Key created successfully!
 
 openpgp-keygen-import-success = OpenPGP Keys imported successfully!
 
+openpgp-keygen-external-success = External GnuPG Key ID saved!
+
 ## OpenPGP Key selection area
 
 openpgp-radio-none =
     .label = None
 
 openpgp-radio-none-desc = Do not use OpenPGP for this identity.
 
 #   $key (String) - the expiration date of the OpenPGP key
@@ -295,16 +297,22 @@ openpgp-key-edit-title = Change OpenPGP 
 openpgp-key-edit-date-title = Extend Expiration Date
 
 openpgp-manager-description = Use the OpenPGP Key Manager to view and manage public keys of your correspondents and all other keys not listed above.
 
 openpgp-manager-button =
     .label = OpenPGP Key Manager
     .accesskey = K
 
+openpgp-key-remove-external =
+    .label = Remove External Key ID
+    .accesskey = E
+
+key-external-label = External GnuPG Key
+
 # Strings in keyDetailsDlg.xhtml
 key-type-public = public key
 key-type-primary = primary key
 key-type-subkey = subkey
 key-type-pair = key pair (secret key and public key)
 key-expiry-never = never
 key-usage-encrypt = Encrypt
 key-usage-sign = Sign
@@ -491,16 +499,20 @@ after-revoke-info =
     As soon as the software used by other people learns about the revocation, it will stop using your old key.
     If you are using a new key for the same email address, and you attach the new public key to emails you send, then information about your revoked old key will be automatically included.
 
 # Strings in keyRing.jsm & decryption.jsm
 key-man-button-import = &Import
 
 delete-key-title = Delete OpenPGP Key
 
+delete-external-key-title = Remove the External GnuPG Key
+
+delete-external-key-description = Do you want to remove this External GnuPG key ID?
+
 key-in-use-title = OpenPGP Key currently in use
 
 delete-key-in-use-description = Unable to proceed! The Key you selected for deletion is currently being used by this identity. Select a different key, or select none, and try again.
 
 revoke-key-in-use-description = Unable to proceed! The Key you selected for revocation is currently being used by this identity. Select a different key, or select none, and try again.
 
 # Strings used in errorHandling.jsm
 key-error-key-spec-not-found = The email address '{ $keySpec }' cannot be matched to a key on your keyring.
--- a/mail/extensions/openpgp/content/strings/key-wizard.ftl
+++ b/mail/extensions/openpgp/content/strings/key-wizard.ftl
@@ -167,8 +167,21 @@ openpgp-import-bits-label = Bits
 
 openpgp-import-key-props =
     .label = Key Properties
     .accesskey = K
 
 ## External Key section
 
 openpgp-external-key-title = External GnuPG Key
+
+openpgp-external-key-description = Configure an external GnuPG key by entering the Key ID
+
+openpgp-external-key-info = In addition, you must use Key Manager to import and accept the corresponding Public Key.
+
+openpgp-external-key-warning = <b>You may configure only one external GnuPG Key.</b> Your previous entry will be replaced.
+
+openpgp-save-external-button = Save key ID
+
+openpgp-external-key-label = Secret Key ID:
+
+openpgp-external-key-input =
+    .placeholder = 123456789341298340
--- a/mail/extensions/openpgp/content/ui/enigmailKeyManager.js
+++ b/mail/extensions/openpgp/content/ui/enigmailKeyManager.js
@@ -1328,16 +1328,17 @@ function openKeyWizard(isImport = false)
       : window.docShell.rootTreeItem.domWindow;
 
   let args = {
     identity: gIdentity,
     gSubDialog: null,
     cancelCallback: clearKeyCache,
     okCallback: clearKeyCache,
     okImportCallback: clearKeyCache,
+    okExternalCallback: clearKeyCache,
     keyDetailsDialog: enigmailKeyDetails,
     isCreate: !isImport,
     isImport,
   };
 
   w.openDialog(
     "chrome://openpgp/content/ui/keyWizard.xhtml",
     "enigmail:KeyWizard",
--- a/mail/extensions/openpgp/content/ui/keyWizard.js
+++ b/mail/extensions/openpgp/content/ui/keyWizard.js
@@ -194,16 +194,17 @@ async function wizardNextStep() {
       await openPgpImportStart();
       break;
 
     case "importComplete":
       openPgpImportComplete();
       break;
 
     case "external":
+      openPgpExternalComplete();
       break;
   }
 }
 
 /**
  * Go back to the initial view of the wizard.
  */
 function goBack() {
@@ -271,19 +272,41 @@ async function wizardCreateKey() {
 function wizardImportKey() {
   kCurrentSection = "import";
   revealSection("wizardImportKey");
 }
 
 /**
  * Show the Key Setup via external smartcard section.
  */
-function wizardExternalKey() {
+async function wizardExternalKey() {
   kCurrentSection = "external";
   revealSection("wizardExternalKey");
+
+  let createLabel = await document.l10n.formatValue(
+    "openpgp-save-external-button"
+  );
+
+  kDialog.getButton("accept").label = createLabel;
+  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) {
@@ -919,8 +942,31 @@ function passphrasePromptCallback(win, k
     overlay.addEventListener("transitionend", hideOverlay);
     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(
+    "last_entered_external_gnupg_key_id",
+    externalKey
+  );
+  gIdentity.setUnicharAttribute("openpgp_key_id", externalKey);
+
+  window.arguments[0].okExternalCallback();
+  window.close();
+}
--- a/mail/extensions/openpgp/content/ui/keyWizard.xhtml
+++ b/mail/extensions/openpgp/content/ui/keyWizard.xhtml
@@ -266,16 +266,46 @@
         <separator class="thin"/>
       </vbox>
     </vbox>
 
     <vbox id="wizardExternalKey" class="wizard-section hide-reverse" hidden="true">
       <label data-l10n-id="openpgp-external-key-title"
              class="dialogheader-title"/>
 
-      <description>
-        Work in Progress: this feature is not yet implemented
-      </description>
+      <html:div>
+        <html:fieldset>
+          <html:legend data-l10n-id="openpgp-external-key-description">
+          </html:legend>
+
+          <description data-l10n-id="openpgp-external-key-info"/>
+
+          <separator/>
 
-      <separator/>
+          <hbox align="center">
+            <label for="externalKey"
+                   data-l10n-id="openpgp-external-key-label"></label>
+            <hbox class="input-container" flex="1">
+              <html:input id="externalKey" type="text"
+                          class="input-inline"
+                          data-l10n-id="openpgp-external-key-input"
+                          oninput="toggleSaveButton(event)">
+              </html:input>
+            </hbox>
+          </hbox>
+
+          <separator/>
+
+          <hbox id="openPgpExternalWarning"
+                class="inline-notification-container info-container"
+                collapsed="true">
+            <hbox class="inline-notification-wrapper">
+              <image class="notification-image notification-image-info"/>
+              <description data-l10n-id="openpgp-external-key-warning"/>
+            </hbox>
+          </hbox>
+        </html:fieldset>
+      </html:div>
+
+      <separator class="thin"/>
     </vbox>
 </dialog>
 </window>
--- a/mail/themes/shared/mail/accountManage.css
+++ b/mail/themes/shared/mail/accountManage.css
@@ -595,8 +595,17 @@ treechildren::-moz-tree-image(folderName
 .intro-paragraph {
   margin-block: 0 6px;
 }
 
 .tip-caption {
   color: var(--in-content-deemphasized-text);
   font-size: .9em;
 }
+
+.external-pill {
+  background-color: #4576B6;
+  color: #fff;
+  font-weight: 600;
+  font-size: 0.9em;
+  padding: 1px 6px;
+  border-radius: 12px;
+}
--- a/mail/themes/shared/openpgp/keyWizard.css
+++ b/mail/themes/shared/openpgp/keyWizard.css
@@ -127,8 +127,18 @@
   color: var(--in-content-deemphasized-text);
   font-size: .9em;
 }
 
 .description-centered {
   text-align: center;
   margin-inline: 20px;
 }
+
+.input-container {
+  display: flex;
+  align-items: center;
+  flex-wrap: nowrap;
+}
+
+.input-container input:not([type="number"]):not([type="color"]) {
+  flex: 1;
+}