diff options
Diffstat (limited to 'comm/mail/extensions/am-e2e')
-rw-r--r-- | comm/mail/extensions/am-e2e/AME2E.jsm | 24 | ||||
-rw-r--r-- | comm/mail/extensions/am-e2e/am-e2e.inc.xhtml | 237 | ||||
-rw-r--r-- | comm/mail/extensions/am-e2e/am-e2e.js | 1591 | ||||
-rw-r--r-- | comm/mail/extensions/am-e2e/am-e2e.xhtml | 32 | ||||
-rw-r--r-- | comm/mail/extensions/am-e2e/components.conf | 15 | ||||
-rw-r--r-- | comm/mail/extensions/am-e2e/moz.build | 16 | ||||
-rw-r--r-- | comm/mail/extensions/am-e2e/prefs/e2e-prefs.js | 285 |
7 files changed, 2200 insertions, 0 deletions
diff --git a/comm/mail/extensions/am-e2e/AME2E.jsm b/comm/mail/extensions/am-e2e/AME2E.jsm new file mode 100644 index 0000000000..86237d3e0e --- /dev/null +++ b/comm/mail/extensions/am-e2e/AME2E.jsm @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +var EXPORTED_SYMBOLS = ["E2EService"]; + +function E2EService() {} + +E2EService.prototype = { + name: "e2e", + chromePackageName: "messenger", + showPanel(server) { + // don't show the panel for news, rss, or local accounts + return ( + server.type != "nntp" && + server.type != "rss" && + server.type != "im" && + server.type != "none" + ); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIMsgAccountManagerExtension"]), +}; diff --git a/comm/mail/extensions/am-e2e/am-e2e.inc.xhtml b/comm/mail/extensions/am-e2e/am-e2e.inc.xhtml new file mode 100644 index 0000000000..76841870aa --- /dev/null +++ b/comm/mail/extensions/am-e2e/am-e2e.inc.xhtml @@ -0,0 +1,237 @@ +# 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/. + + <vbox id="e2eEditing"> + + <stringbundle id="bundle_e2e" src="chrome://messenger/locale/am-smime.properties"/> + <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/> + + <linkset> + <html:link rel="localization" href="branding/brand.ftl"/> + <html:link rel="localization" href="messenger/openpgp/openpgp.ftl"/> + </linkset> + + <vbox flex="1"> + <html:p class="intro-paragraph">&e2eEnc.description;</html:p> + <html:p class="intro-paragraph" + data-l10n-id="e2e-intro-description"></html:p> + <html:span class="tail-with-learn-more" + data-l10n-id="e2e-intro-description-more"></html:span> + <label is="text-link" id="acceptLearnMoreE2E" + href="https://support.mozilla.org/kb/thunderbird-help-setup-account-e2ee" + value="&e2eLearnMore.label;"/> + </vbox> + + <html:div> + <html:fieldset id="openpgpOptions" + aria-describedby="openPgpDescription"> + <html:legend>&openpgpKeys.label;</html:legend> + + <vbox data-subcategory="openpgp" class="openpgp-container"> + <hbox align="center" class="opengpg-intro-section"> + <html:img id="openPgpKey" + src="chrome://messenger/skin/icons/new/compact/key.svg" + alt="" /> + <vbox flex="1"> + <description class="description-with-side-element openpgp-description"> + <html:p id="openPgpDescription"></html:p> + <html:img id="openPgpStatusImage" class="openpgp-status" + alt="" hidden="hidden"/> + <html:span id="openPgpSelectionStatus" + class="tail-with-learn-more" + hidden="hidden"></html:span> + <label is="text-link" id="openPgpLearnMore" + href="https://support.mozilla.org/kb/introduction-to-e2e-encryption" + data-l10n-id="e2e-learn-more" + class="learnMore" + hidden="true"/> + </description> + </vbox> + <vbox> + <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. --> + <hbox> + <button id="addOpenPgpButton" + data-l10n-id="openpgp-add-key-button" + oncommand="openKeyWizard();" + class="accessory-button openpgp-add-key-button openpgp-image-btn" + flex="1"/> + </hbox> + </vbox> + </hbox> + + <hbox id="openPgpNotification" + class="inline-notification-container success-container" + collapsed="true"> + <hbox class="inline-notification-wrapper align-center"> + <html:img class="notification-image" + src="chrome://global/skin/icons/check.svg" + alt="" /> + <description id="openPgpNotificationDescription"/> + <button class="close-icon" oncommand="closeNotification()"/> + </hbox> + </hbox> + + <vbox id="openPgpKeyList"> + <radiogroup id="openPgpKeyListRadio"> + <vbox id="openPgpOptionNone" class="content-blocking-category"> + <hbox> + <radio id="openPgpNone" + value="" + data-l10n-id="openpgp-radio-none" + flex="1"/> + </hbox> + <vbox class="indent"> + <description data-l10n-id="openpgp-radio-none-desc"/> + </vbox> + </vbox> + <!-- All available keys will be appended here. --> + </radiogroup> + </vbox> + </vbox> + + <separator/> + + <description flex="1" data-l10n-id="openpgp-manager-description"/> + + <separator class="thin"/> + + <hbox> + <button id="openOpenPGPKeyManagerButton" + data-l10n-id="openpgp-manager-button" + class="first-element" + oncommand="openKeyManager()"/> + </hbox> + </html:fieldset> + </html:div> + + <separator/> + + <html:div> + <html:fieldset id="smimeOptions"> + <html:legend>&certificates2.label;</html:legend> + + <label id="identity_signing_cert_nameLabel" + value="&signingCert2.message;" control="identity_signing_cert_name"/> + + <hbox align="center" class="input-container"> + <html:input id="identity_signing_cert_name" type="text" + class="input-inline" + readonly="readonly" + disabled="disabled" + aria-labelledby="identity_signing_cert_nameLabel" + wsm_persist="true" + prefstring="mail.identity.%identitykey%.signing_cert_name"/> + + <button id="signingCertSelectButton" + label="&digitalSign.certificate.button;" + accesskey="&digitalSign.certificate.accesskey;" + oncommand="smimeSelectCert('identity_signing_cert_name')"/> + + <button id="signingCertClearButton" + label="&digitalSign.certificate_clear.button;" + accesskey="&digitalSign.certificate_clear.accesskey;" + oncommand="smimeClearCert('identity_signing_cert_name')"/> + </hbox> + + <separator class="thin"/> + + <label value="&encryptionCert2.message;" + control="identity_encryption_cert_name"/> + + <hbox align="center" class="input-container"> + <html:input id="identity_encryption_cert_name" type="text" + class="input-inline" + readonly="readonly" + disabled="disabled" + wsm_persist="true" + prefstring="mail.identity.%identitykey%.encryption_cert_name"/> + + <button id="encryptionCertSelectButton" + label="&encryption.certificate.button;" + accesskey="&encryption.certificate.accesskey;" + oncommand="smimeSelectCert('identity_encryption_cert_name')"/> + + <button id="encryptionCertClearButton" + label="&encryption.certificate_clear.button;" + accesskey="&encryption.certificate_clear.accesskey;" + oncommand="smimeClearCert('identity_encryption_cert_name')"/> + </hbox> + + <separator class="thin"/> + + <hbox align="right"> + <button id="openCertManagerButton" oncommand="openCertManager();" + label="&manageCerts3.label;" accesskey="&manageCerts3.accesskey;"/> + <button id="openDeviceManagerButton" oncommand="openDeviceManager();" + label="&manageDevices2.label;" accesskey="&manageDevices2.accesskey;"/> + </hbox> + + </html:fieldset> + </html:div> + + <separator/> + + <html:legend>&sendingDefaults.label;</html:legend> + + <html:div> + <html:fieldset> + <radiogroup id="encryptionChoices" class="indent" hidden="true"> + <radio id="disable_encryption" wsm_persist="true" value="0" + data-l10n-id="e2e-disable-enc"/> + <radio id="enable_encryption" wsm_persist="true" value="2" + data-l10n-id="e2e-enable-enc"/> + <hbox> + <description flex="1" data-l10n-id="e2e-enable-description" + class="option-description tip-caption"/> + </hbox> + </radiogroup> + + <separator/> + + <description flex="1" data-l10n-id="e2e-signing-description"></description> + <checkbox id="identity_sign_mail" class="indent" wsm_persist="true" + prefstring="mail.identity.%identitykey%.sign_mail" + data-l10n-id="e2e-sign-message"/> + + </html:fieldset> + </html:div> + + <separator/> + + <html:legend data-l10n-id="e2e-advanced-section"/> + + <html:div> + <html:fieldset> + <checkbox id="identity_attach_key" wsm_persist="true" + prefstring="mail.identity.%identitykey%.attachPgpKey" + data-l10n-id="e2e-attach-key"/> + + <checkbox id="identity_autocrypt_headers" wsm_persist="true" + prefstring="mail.identity.%identitykey%.sendAutocryptHeaders" + data-l10n-id="e2e-autocrypt-headers"/> + + <checkbox id="identity_encrypt_subject" wsm_persist="true" + prefstring="mail.identity.%identitykey%.protectSubject" + data-l10n-id="e2e-encrypt-subject"/> + + <checkbox id="identity_encrypt_drafts" wsm_persist="true" + prefstring="mail.identity.%identitykey%.autoEncryptDrafts" + data-l10n-id="e2e-encrypt-drafts"/> + + <separator/> + + <description>&e2eTechPref.description;</description> + <radiogroup id="technologyChoices" class="indent"> + <radio id="technology_automatic" wsm_persist="true" value="0" + label="&technologyAutomatic.label;"/> + + <radio id="technology_prefer_openpgp" wsm_persist="true" value="2" + label="&technologyOpenPGP.label;"/> + + <radio id="technology_prefer_smime" wsm_persist="true" value="1" + label="&technologySMIME.label;"/> + </radiogroup> + </html:fieldset> + </html:div> + </vbox> diff --git a/comm/mail/extensions/am-e2e/am-e2e.js b/comm/mail/extensions/am-e2e/am-e2e.js new file mode 100644 index 0000000000..1926d38d32 --- /dev/null +++ b/comm/mail/extensions/am-e2e/am-e2e.js @@ -0,0 +1,1591 @@ +/* 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/. */ + +/* import-globals-from ../../../../toolkit/content/preferencesBindings.js */ +/* import-globals-from ../../../mailnews/base/prefs/content/am-identity-edit.js */ + +/* global GetEnigmailSvc, EnigRevokeKey */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm"); +var { EnigmailKey } = ChromeUtils.import( + "chrome://openpgp/content/modules/key.jsm" +); +var { EnigmailDialog } = ChromeUtils.import( + "chrome://openpgp/content/modules/dialog.jsm" +); +var { EnigmailKeyRing } = ChromeUtils.import( + "chrome://openpgp/content/modules/keyRing.jsm" +); +var { EnigmailKeyserverURIs } = ChromeUtils.import( + "chrome://openpgp/content/modules/keyserverUris.jsm" +); +var { EnigmailKeyServer } = ChromeUtils.import( + "chrome://openpgp/content/modules/keyserver.jsm" +); +var { EnigmailCryptoAPI } = ChromeUtils.import( + "chrome://openpgp/content/modules/cryptoAPI.jsm" +); +var { PgpSqliteDb2 } = ChromeUtils.import( + "chrome://openpgp/content/modules/sqliteDb.jsm" +); + +var email_signing_cert_usage = 4; // SECCertUsage.certUsageEmailSigner +var email_recipient_cert_usage = 5; // SECCertUsage.certUsageEmailRecipient + +var gIdentity; +var gEncryptionCertName = null; +var gEncryptionChoices = null; +var gSignCertName = null; +var gTechChoices = null; +var gSignMessages = null; +var gRequireEncrypt = null; +var gDoNotEncrypt = null; +var gAttachKey = null; +var gSendAutocryptHeaders = null; +var gEncryptSubject = null; +var gEncryptDrafts = null; + +var gKeyId = null; // "" will denote selection 'None'. +var gBundle = null; +var gBrandBundle; +var gSmimePrefbranch; +var kEncryptionCertPref = "identity_encryption_cert_name"; +var kSigningCertPref = "identity_signing_cert_name"; + +var gTechAuto = null; +var gTechPrefOpenPGP = null; +var gTechPrefSMIME = null; + +function onInit() { + initE2EEncryption(gIdentity); + Services.prefs.addObserver("mail.e2ee.auto_enable", autoEncryptPrefObserver); + Services.prefs.addObserver("mail.e2ee.auto_disable", autoEncryptPrefObserver); +} + +window.addEventListener("unload", function () { + Services.prefs.removeObserver( + "mail.e2ee.auto_enable", + autoEncryptPrefObserver + ); + Services.prefs.removeObserver( + "mail.e2ee.auto_disable", + autoEncryptPrefObserver + ); +}); + +let gDisableEncryption; +let gEnableEncryption; + +var autoEncryptPrefObserver = { + observe(subject, topic, prefName) { + if (topic == "nsPref:changed") { + if ( + prefName == "mail.e2ee.auto_enable" || + prefName == "mail.e2ee.auto_disable" + ) { + updateAutoEncryptRelated(); + } + } + }, +}; + +function updateAutoEncryptRelated() { + if (Services.prefs.getBoolPref("mail.e2ee.auto_enable")) { + document.getElementById("encryptionChoices").hidden = true; + } else { + document.getElementById("encryptionChoices").hidden = false; + } +} + +async function initE2EEncryption(identity) { + // Initialize all of our elements based on the current identity values... + gEncryptionCertName = document.getElementById(kEncryptionCertPref); + gEncryptionChoices = document.getElementById("encryptionChoices"); + gSignCertName = document.getElementById(kSigningCertPref); + gSignMessages = document.getElementById("identity_sign_mail"); + gDisableEncryption = document.getElementById("disable_encryption"); + gEnableEncryption = document.getElementById("enable_encryption"); + gAttachKey = document.getElementById("identity_attach_key"); + gSendAutocryptHeaders = document.getElementById("identity_autocrypt_headers"); + gEncryptSubject = document.getElementById("identity_encrypt_subject"); + gEncryptDrafts = document.getElementById("identity_encrypt_drafts"); + + gBundle = document.getElementById("bundle_e2e"); + gBrandBundle = document.getElementById("bundle_brand"); + + gTechChoices = document.getElementById("technologyChoices"); + gTechAuto = document.getElementById("technology_automatic"); + gTechPrefOpenPGP = document.getElementById("technology_prefer_openpgp"); + gTechPrefSMIME = document.getElementById("technology_prefer_smime"); + + if (!identity) { + // We're setting up a new identity. Set most prefs to default values. + // Only take selected values from gAccount.defaultIdentity + // as the new identity is going to have a different mail address. + + gEncryptionCertName.value = ""; + gEncryptionCertName.displayName = ""; + gEncryptionCertName.dbKey = ""; + + gSignCertName.value = ""; + gSignCertName.displayName = ""; + gSignCertName.dbKey = ""; + + gDisableEncryption.disabled = true; + gEnableEncryption.disabled = true; + gEncryptSubject.disabled = true; + gEncryptDrafts.disabled = true; + gSignMessages.disabled = true; + + gAttachKey.checked = gAccount.defaultIdentity.attachPgpKey; + gSendAutocryptHeaders.checked = + gAccount.defaultIdentity.sendAutocryptHeaders; + gEncryptSubject.checked = gAccount.defaultIdentity.protectSubject; + gEncryptDrafts.checked = gAccount.defaultIdentity.autoEncryptDrafts; + gSignMessages.checked = gAccount.defaultIdentity.signMail; + gEncryptionChoices.value = gAccount.defaultIdentity.encryptionPolicy; + + gTechChoices.value = 0; + } else { + // We're editing an existing identity. + + initSMIMESettings(); + await initOpenPgpSettings(); + + let enableEnc = !!gEncryptionCertName.value; + enableEnc = enableEnc || !!gKeyId; + enableEncryptionControls(enableEnc); + + gSignMessages.checked = identity.signMail; + gAttachKey.checked = identity.attachPgpKey; + gSendAutocryptHeaders.checked = identity.sendAutocryptHeaders; + gEncryptSubject.checked = identity.protectSubject; + gEncryptDrafts.checked = identity.autoEncryptDrafts; + + let enableSig = gSignCertName.value; + enableSig = enableSig || !!gKeyId; + enableSigningControls(enableSig); + } + + updateAutoEncryptRelated(); + + // Always start with enabling select buttons. + // This will keep the visibility of buttons in a sane state as user + // jumps from security panel of one account to another. + enableSelectButtons(); + updateTechPref(); +} + +/** + * Initialize the S/MIME settings based on identity preferences. + */ +function initSMIMESettings() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + gEncryptionCertName.value = gIdentity.getUnicharAttribute( + "encryption_cert_name" + ); + gEncryptionCertName.dbKey = gIdentity.getCharAttribute( + "encryption_cert_dbkey" + ); + // If we succeed in looking up the certificate by the dbkey pref, then + // append the serial number " [...]" to the display value, and remember the + // displayName in a separate property. + try { + let x509cert = null; + if ( + gEncryptionCertName.dbKey && + (x509cert = certdb.findCertByDBKey(gEncryptionCertName.dbKey)) + ) { + gEncryptionCertName.value = + x509cert.displayName + " [" + x509cert.serialNumber + "]"; + gEncryptionCertName.displayName = x509cert.displayName; + } + } catch (e) {} + + gEncryptionChoices.value = gIdentity.encryptionPolicy; + gTechChoices.value = gIdentity.getIntAttribute("e2etechpref"); + + gSignCertName.value = gIdentity.getUnicharAttribute("signing_cert_name"); + gSignCertName.dbKey = gIdentity.getCharAttribute("signing_cert_dbkey"); + + // same procedure as with gEncryptionCertName (see above) + try { + let x509cert = null; + if ( + gSignCertName.dbKey && + (x509cert = certdb.findCertByDBKey(gSignCertName.dbKey)) + ) { + gSignCertName.value = + x509cert.displayName + " [" + x509cert.serialNumber + "]"; + gSignCertName.displayName = x509cert.displayName; + } + } catch (e) {} +} + +/** + * Initialize the OpenPGP settings, apply strings, and load the key radio UI. + */ +async function initOpenPgpSettings() { + let result = {}; + await EnigmailKeyRing.getAllSecretKeysByEmail(gIdentity.email, result, true); + + let externalKey = gIdentity.getUnicharAttribute( + "last_entered_external_gnupg_key_id" + ); + + let keyCount = result.all.length + (externalKey ? 1 : 0); + if (keyCount) { + document.l10n.setAttributes( + document.getElementById("openPgpDescription"), + "openpgp-description-has-keys", + { + count: keyCount, + identity: gIdentity.email, + } + ); + } else { + document.l10n.setAttributes( + document.getElementById("openPgpDescription"), + "openpgp-description-no-key", + { + identity: gIdentity.email, + } + ); + } + + closeNotification(); + + let keyId = gIdentity.getUnicharAttribute("openpgp_key_id"); + useOpenPGPKey(keyId); + + // When key changes, update settings. + let openPgpKeyListRadio = document.getElementById("openPgpKeyListRadio"); + openPgpKeyListRadio.addEventListener("command", event => { + closeNotification(); + useOpenPGPKey(event.target.value); + }); +} + +function onPreInit(account, accountValues) { + gIdentity = account.defaultIdentity; +} + +// NOTE: AccountManager.js checks and calls "onSave" in savePage. +function onSave() { + saveE2EEncryptionSettings(gIdentity); +} + +function saveE2EEncryptionSettings(identity) { + // Find out which radio for the encryption radio group is selected and set + // that on our hidden encryptionChoice pref. + let newValue = gEncryptionChoices.value; + identity.encryptionPolicy = newValue; + + newValue = gTechChoices.value; + identity.setIntAttribute("e2etechpref", newValue); + + identity.setUnicharAttribute( + "encryption_cert_name", + gEncryptionCertName.displayName || gEncryptionCertName.value + ); + identity.setCharAttribute("encryption_cert_dbkey", gEncryptionCertName.dbKey); + + identity.signMail = gSignMessages.checked; + identity.setUnicharAttribute( + "signing_cert_name", + gSignCertName.displayName || gSignCertName.value + ); + identity.setCharAttribute("signing_cert_dbkey", gSignCertName.dbKey); + + identity.attachPgpKey = gAttachKey.checked; + identity.sendAutocryptHeaders = gSendAutocryptHeaders.checked; + identity.protectSubject = gEncryptSubject.checked; + identity.autoEncryptDrafts = gEncryptDrafts.checked; +} + +function alertUser(message) { + Services.prompt.alert( + window, + gBrandBundle.getString("brandShortName"), + message + ); +} + +function askUser(message) { + let button = Services.prompt.confirmEx( + window, + gBrandBundle.getString("brandShortName"), + message, + Services.prompt.STD_YES_NO_BUTTONS, + null, + null, + null, + null, + {} + ); + // confirmEx returns button index: + return button == 0; +} + +function checkOtherCert( + cert, + pref, + usage, + msgNeedCertWantSame, + msgWantSame, + msgNeedCertWantToSelect, + enabler +) { + var otherCertInfo = document.getElementById(pref); + if (otherCertInfo.dbKey == cert.dbKey) { + // All is fine, same cert is now selected for both purposes. + return; + } + + var secMsg = Cc["@mozilla.org/nsCMSSecureMessage;1"].getService( + Ci.nsICMSSecureMessage + ); + + var matchingOtherCert; + if (email_recipient_cert_usage == usage) { + if (secMsg.canBeUsedForEmailEncryption(cert)) { + matchingOtherCert = cert; + } + } else if (email_signing_cert_usage == usage) { + if (secMsg.canBeUsedForEmailSigning(cert)) { + matchingOtherCert = cert; + } + } else { + throw new Error("Unexpected SECCertUsage: " + usage); + } + + var userWantsSameCert = false; + if (!otherCertInfo.value) { + if (matchingOtherCert) { + userWantsSameCert = askUser(gBundle.getString(msgNeedCertWantSame)); + } else if (askUser(gBundle.getString(msgNeedCertWantToSelect))) { + smimeSelectCert(pref); + } + } else if (matchingOtherCert) { + userWantsSameCert = askUser(gBundle.getString(msgWantSame)); + } + + if (userWantsSameCert) { + otherCertInfo.value = cert.displayName + " [" + cert.serialNumber + "]"; + otherCertInfo.displayName = cert.displayName; + otherCertInfo.dbKey = cert.dbKey; + enabler(true); + } +} + +function smimeSelectCert(smime_cert) { + var certInfo = document.getElementById(smime_cert); + if (!certInfo) { + return; + } + + var picker = Cc["@mozilla.org/user_cert_picker;1"].createInstance( + Ci.nsIUserCertPicker + ); + var canceled = {}; + var x509cert; + var certUsage; + var selectEncryptionCert; + + if (smime_cert == kEncryptionCertPref) { + selectEncryptionCert = true; + certUsage = email_recipient_cert_usage; + } else if (smime_cert == kSigningCertPref) { + selectEncryptionCert = false; + certUsage = email_signing_cert_usage; + } + + try { + x509cert = picker.pickByUsage( + window, + certInfo.value, + certUsage, // this is from enum SECCertUsage + false, + true, + gIdentity.email, + canceled + ); + } catch (e) { + canceled.value = false; + x509cert = null; + } + + if (!canceled.value) { + if (!x509cert) { + if (gIdentity.email) { + alertUser( + gBundle.getFormattedString( + selectEncryptionCert + ? "NoEncryptionCertForThisAddress" + : "NoSigningCertForThisAddress", + [gIdentity.email] + ) + ); + } else { + alertUser( + gBundle.getString( + selectEncryptionCert ? "NoEncryptionCert" : "NoSigningCert" + ) + ); + } + } else { + certInfo.disabled = false; + certInfo.value = + x509cert.displayName + " [" + x509cert.serialNumber + "]"; + certInfo.displayName = x509cert.displayName; + certInfo.dbKey = x509cert.dbKey; + + if (selectEncryptionCert) { + enableEncryptionControls(true); + + checkOtherCert( + x509cert, + kSigningCertPref, + email_signing_cert_usage, + "signing_needCertWantSame", + "signing_wantSame", + "signing_needCertWantToSelect", + enableSigningControls + ); + } else { + enableSigningControls(true); + + checkOtherCert( + x509cert, + kEncryptionCertPref, + email_recipient_cert_usage, + "encryption_needCertWantSame", + "encryption_wantSame", + "encryption_needCertWantToSelect", + enableEncryptionControls + ); + } + } + } + + updateTechPref(); + enableSelectButtons(); + onSave(); +} + +function enableEncryptionControls(do_enable) { + gDisableEncryption.disabled = !do_enable; + gEnableEncryption.disabled = !do_enable; + if (!do_enable) { + gEncryptionChoices.value = 0; + } + // If we have a certificate or key configured that allows encryption, + // then we are able to encrypt drafts, too. + gEncryptDrafts.disabled = !do_enable; +} + +function enableSigningControls(do_enable) { + gSignMessages.disabled = !do_enable; + if (!do_enable) { + gSignMessages.checked = false; + } +} + +function enableSelectButtons() { + gSignCertName.disabled = !gSignCertName.value; + document.getElementById("signingCertClearButton").disabled = + !gSignCertName.value; + + gEncryptionCertName.disabled = !gEncryptionCertName.value; + document.getElementById("encryptionCertClearButton").disabled = + !gEncryptionCertName.value; +} + +function smimeClearCert(smime_cert) { + var certInfo = document.getElementById(smime_cert); + if (!certInfo) { + return; + } + + certInfo.disabled = true; + certInfo.value = ""; + certInfo.displayName = ""; + certInfo.dbKey = ""; + + let stillHaveOther = false; + stillHaveOther = gKeyId != ""; + + if (!stillHaveOther) { + if (smime_cert == kEncryptionCertPref) { + enableEncryptionControls(false); + } else if (smime_cert == kSigningCertPref) { + enableSigningControls(false); + } + } + + updateTechPref(); + enableSelectButtons(); + onSave(); +} + +function updateTechPref() { + let haveSigCert = gSignCertName && gSignCertName.value; + let haveEncCert = gEncryptionCertName && gEncryptionCertName.value; + let havePgpkey = !!gKeyId; + + let enable = (haveSigCert || haveEncCert) && havePgpkey; + + gTechAuto.disabled = !enable; + gTechPrefOpenPGP.disabled = !enable; + gTechPrefSMIME.disabled = !enable; + + if (!enable) { + gTechChoices.value = 0; + } +} + +function openCertManager() { + parent.gSubDialog.open("chrome://pippki/content/certManager.xhtml"); +} + +function openDeviceManager() { + parent.gSubDialog.open("chrome://pippki/content/device_manager.xhtml"); +} + +/** + * Open the OpenPGP Key Manager. + */ +function openKeyManager() { + window.browsingContext.topChromeWindow.openDialog( + "chrome://openpgp/content/ui/enigmailKeyManager.xhtml", + "enigmail:KeyManager", + "dialog,centerscreen,resizable", + { + cancelCallback: reloadOpenPgpUI, + okCallback: reloadOpenPgpUI, + } + ); +} + +/** + * Open the subdialog to create or import an OpenPGP key. + */ +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", + undefined, + args + ); +} + +/** + * Show a successful notification after a new OpenPGP key was created, and + * trigger the reload of the key listing UI. + * + * @param {string} keyId - Id of key that the key wizard set up. + */ +async function keyWizardSuccess(keyId) { + document.l10n.setAttributes( + document.getElementById("openPgpNotificationDescription"), + "openpgp-keygen-success" + ); + document.getElementById("openPgpNotification").collapsed = false; + + useOpenPGPKey(keyId); +} + +/** + * Show a successful notification after an external key was saved, and trigger + * the reload of the key listing UI. + * + * @param {string} keyId - Id of key that the key wizard set up. + */ +async function keyExternalSuccess(keyId) { + document.l10n.setAttributes( + document.getElementById("openPgpNotificationDescription"), + "openpgp-keygen-external-success" + ); + document.getElementById("openPgpNotification").collapsed = false; + + gIdentity.setUnicharAttribute("last_entered_external_gnupg_key_id", keyId); + useOpenPGPKey(keyId); +} + +/** + * Adjust the key listing to account for newly created keys. Then set + * the current identity to start using this key and adjust the UI elements + * to be enabled now that there's a key to use. + * + * NOTE! Please always go through this to change gKeyId! + * + * @param {string} keyId - Id of key that the key wizard set up. + */ +function useOpenPGPKey(keyId) { + // Rebuild the UI so that any new keys are listed. + gKeyId = keyId.toUpperCase(); + + // Update the identity with the key obtained from the key wizard. + gIdentity.setUnicharAttribute("openpgp_key_id", keyId || ""); + + // Always update the GnuPG boolean pref to be sure the currently used key is + // internal or external. + gIdentity.setBoolAttribute( + "is_gnupg_key_id", + gKeyId == + gIdentity.getUnicharAttribute("last_entered_external_gnupg_key_id") + ); + + reloadOpenPgpUI(); +} + +/** + * Show a successful 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; + + 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); + let keyCount = result.all.length; + + let externalKey = null; + if (Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg")) { + externalKey = gIdentity.getUnicharAttribute( + "last_entered_external_gnupg_key_id" + ); + if (externalKey) { + keyCount++; + } + } + + // Show the radiogroup container only if the current identity has keys. + // But still show it if a key (missing or unusable) is configured. + document.getElementById("openPgpKeyList").hidden = keyCount == 0 && !gKeyId; + + // Update the OpenPGP intro description with the current key count. + if (keyCount) { + document.l10n.setAttributes( + document.getElementById("openPgpDescription"), + "openpgp-description-has-keys", + { + count: keyCount, + identity: gIdentity.email, + } + ); + } else { + document.l10n.setAttributes( + document.getElementById("openPgpDescription"), + "openpgp-description-no-key", + { + identity: gIdentity.email, + } + ); + } + + let radiogroup = document.getElementById("openPgpKeyListRadio"); + + if (!gKeyId) { + radiogroup.selectedIndex = 0; // None + } + + // Remove all the previously generated radio options, except the first. + while (radiogroup.lastChild.id != "openPgpOptionNone") { + radiogroup.removeChild(radiogroup.lastChild); + } + + // Currently configured key is not in available, maybe deleted by the user? + if (gKeyId && !externalKey && !result.all.find(key => key.keyId == gKeyId)) { + let container = document.createXULElement("vbox"); + container.id = `openPgpOption${gKeyId}`; + container.classList.add("content-blocking-category"); + + let box = document.createXULElement("hbox"); + let radio = document.createXULElement("radio"); + radio.setAttribute("flex", "1"); + radio.disabled = true; + radio.id = `openPgp${gKeyId}`; + radio.value = gKeyId; + radio.label = `0x${gKeyId}`; + box.appendChild(radio); + + let box2 = document.createXULElement("vbox"); + box2.classList.add("indent"); + let desc = document.createXULElement("description"); + box2.appendChild(desc); + + let key = EnigmailKeyRing.getKeyById(gKeyId); + if (key && !key.secretAvailable) { + document.l10n.setAttributes(desc, "openpgp-radio-key-not-usable"); + } else if (key && !(await PgpSqliteDb2.isAcceptedAsPersonalKey(key.fpr))) { + document.l10n.setAttributes(desc, "openpgp-radio-key-not-accepted"); + let btnContainer = document.createXULElement("hbox"); + btnContainer.setAttribute("pack", "end"); + btnContainer.style.width = "100%"; + let info = document.createXULElement("button"); + info.classList.add("openpgp-image-btn", "openpgp-props-btn"); + document.l10n.setAttributes(info, "openpgp-key-man-key-props"); + info.addEventListener("command", event => { + event.stopPropagation(); + enigmailKeyDetails(key.keyId); + }); + btnContainer.appendChild(info); + box2.appendChild(btnContainer); + } else { + document.l10n.setAttributes(desc, "openpgp-radio-key-not-found"); + } + + container.appendChild(box); + container.appendChild(box2); + radiogroup.appendChild(container); + } + + // 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, and the allow_external_gnupg + // pref is true, 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}`; + + 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"); + + let radio = document.createXULElement("radio"); + radio.setAttribute("flex", "1"); + radio.id = `openPgp${key.keyId}`; + radio.value = key.keyId; + radio.label = `0x${key.keyId}`; + + let toggle = document.createXULElement("button"); + toggle.classList.add("arrowhead"); + toggle.setAttribute("aria-expanded", "false"); + document.l10n.setAttributes(toggle, "openpgp-key-expand-section"); + toggle.addEventListener("command", toggleExpansion); + + box.appendChild(radio); + box.appendChild(toggle); + + 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 dateIcon = document.createElement("img"); + dateIcon.classList.add("expiration-date-icon"); + + let dateButton = document.createXULElement("button"); + document.l10n.setAttributes(dateButton, "openpgp-key-man-change-expiry"); + dateButton.addEventListener("command", event => { + event.stopPropagation(); + enigmailEditKeyDate(key); + }); + dateButton.setAttribute("hidden", "true"); + dateButton.classList.add("button-small"); + + let description = document.createXULElement("description"); + + if (key.expiryTime) { + if (Math.round(Date.now() / 1000) > key.expiryTime) { + // Has expired. + dateContainer.classList.add("key-expired"); + dateIcon.setAttribute( + "src", + "chrome://messenger/skin/icons/new/compact/warning.svg" + ); + // Sets the title attribute. + // The alt attribute is not set because the accessible name is already + // set by the title. + document.l10n.setAttributes(dateIcon, "openpgp-key-has-expired-icon"); + + document.l10n.setAttributes(description, "openpgp-radio-key-expired", { + date: key.expiry, + }); + + dateButton.removeAttribute("hidden"); + // This key is expired, so make it unselectable. + radio.setAttribute("disabled", "true"); + } else { + // If the key expires in less than 6 months. + let sixMonths = new Date(); + sixMonths.setMonth(sixMonths.getMonth() + 6); + if (Math.round(Date.parse(sixMonths) / 1000) > key.expiryTime) { + dateContainer.classList.add("key-is-expiring"); + dateIcon.setAttribute( + "src", + "chrome://messenger/skin/icons/new/compact/info.svg" + ); + // Sets the title attribute. + // The alt attribute is not set because the accessible name is already + // set by the title. + document.l10n.setAttributes( + dateIcon, + "openpgp-key-expires-within-6-months-icon" + ); + dateButton.removeAttribute("hidden"); + } + + document.l10n.setAttributes(description, "openpgp-radio-key-expires", { + date: key.expiry, + }); + } + } else { + document.l10n.setAttributes(description, "key-does-not-expire"); + } + + dateContainer.appendChild(dateIcon); + dateContainer.appendChild(description); + dateContainer.appendChild(dateButton); + + let publishContainer = null; + + // If this key is the currently selected key, suggest publishing. + if (key.keyId == gKeyId) { + publishContainer = document.createXULElement("hbox"); + publishContainer.setAttribute("align", "center"); + + let publishButton = document.createElement("button"); + document.l10n.setAttributes(publishButton, "openpgp-key-publish"); + publishButton.addEventListener("click", () => { + amE2eUploadKey(key); + }); + publishButton.classList.add("button-small"); + + let description = document.createXULElement("description"); + document.l10n.setAttributes( + description, + "openpgp-suggest-publishing-key" + ); + + publishContainer.appendChild(description); + publishContainer.appendChild(publishButton); + } + + let hiddenContainer = document.createXULElement("vbox"); + hiddenContainer.classList.add( + "content-blocking-extra-information", + "indent" + ); + + // Start key info section. + let grid = document.createXULElement("hbox"); + grid.classList.add("extra-information-label"); + + // Key fingerprint. + let fingerprintImage = document.createElement("img"); + fingerprintImage.setAttribute( + "src", + "chrome://messenger/skin/icons/new/compact/fingerprint.svg" + ); + fingerprintImage.setAttribute("alt", ""); + + let fingerprintLabel = document.createXULElement("label"); + document.l10n.setAttributes( + fingerprintLabel, + "openpgp-key-details-fingerprint-label" + ); + fingerprintLabel.classList.add("extra-information-label-type"); + + let fgrInputContainer = document.createXULElement("hbox"); + fgrInputContainer.classList.add("input-container"); + fgrInputContainer.setAttribute("flex", "1"); + + let fingerprintInput = document.createElement("input"); + fingerprintInput.setAttribute("type", "text"); + fingerprintInput.classList.add("plain"); + fingerprintInput.setAttribute("readonly", "readonly"); + fingerprintInput.value = EnigmailKey.formatFpr(key.fpr); + + fgrInputContainer.appendChild(fingerprintInput); + + grid.appendChild(fingerprintImage); + grid.appendChild(fingerprintLabel); + grid.appendChild(fgrInputContainer); + + // Key creation date. + let createdImage = document.createElement("img"); + createdImage.setAttribute( + "src", + "chrome://messenger/skin/icons/new/compact/calendar.svg" + ); + createdImage.setAttribute("alt", ""); + + let createdLabel = document.createXULElement("label"); + document.l10n.setAttributes( + createdLabel, + "openpgp-key-details-created-header" + ); + createdLabel.classList.add("extra-information-label-type"); + + let createdValueContainer = document.createXULElement("hbox"); + createdValueContainer.classList.add("input-container"); + createdValueContainer.setAttribute("flex", "1"); + + let createdValue = document.createElement("input"); + createdValue.setAttribute("type", "text"); + createdValue.classList.add("plain"); + createdValue.setAttribute("readonly", "readonly"); + createdValue.value = key.created; + + createdValueContainer.appendChild(createdValue); + + grid.appendChild(createdImage); + grid.appendChild(createdLabel); + grid.appendChild(createdValueContainer); + // End key info section. + + hiddenContainer.appendChild(grid); + + // Action buttons. + let btnContainer = document.createXULElement("hbox"); + btnContainer.setAttribute("pack", "end"); + + let info = document.createXULElement("button"); + info.classList.add("openpgp-image-btn", "openpgp-props-btn"); + document.l10n.setAttributes(info, "openpgp-key-man-key-props"); + info.addEventListener("command", event => { + event.stopPropagation(); + enigmailKeyDetails(key.keyId); + }); + + let more = document.createXULElement("button"); + more.setAttribute("type", "menu"); + more.classList.add("openpgp-more-btn", "last-element"); + document.l10n.setAttributes(more, "openpgp-key-man-key-more"); + + let menupopup = document.createXULElement("menupopup"); + + let copyItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(copyItem, "openpgp-key-copy-key"); + copyItem.addEventListener("command", event => { + event.stopPropagation(); + openPgpCopyToClipboard(`0x${key.keyId}`); + }); + + let sendItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(sendItem, "openpgp-key-send-key"); + sendItem.addEventListener("command", event => { + event.stopPropagation(); + openPgpSendKeyEmail(`0x${key.keyId}`); + }); + + let exportItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(exportItem, "openpgp-key-export-key"); + exportItem.addEventListener("command", event => { + event.stopPropagation(); + openPgpExportPublicKey(`0x${key.keyId}`); + }); + + let backupItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(backupItem, "openpgp-key-backup-key"); + backupItem.addEventListener("command", event => { + event.stopPropagation(); + openPgpExportSecretKey(`0x${key.keyId}`, `${key.fpr}`); + }); + + let revokeItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(revokeItem, "openpgp-key-man-revoke-key"); + revokeItem.addEventListener("command", event => { + event.stopPropagation(); + openPgpRevokeKey(key); + }); + + let deleteItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(deleteItem, "openpgp-delete-key"); + deleteItem.addEventListener("command", event => { + event.stopPropagation(); + enigmailDeleteKey(key); + }); + + menupopup.appendChild(copyItem); + menupopup.appendChild(sendItem); + menupopup.appendChild(exportItem); + menupopup.appendChild(document.createXULElement("menuseparator")); + menupopup.appendChild(backupItem); + menupopup.appendChild(document.createXULElement("menuseparator")); + menupopup.appendChild(revokeItem); + menupopup.appendChild(deleteItem); + + more.appendChild(menupopup); + + btnContainer.appendChild(info); + btnContainer.appendChild(more); + + hiddenContainer.appendChild(btnContainer); + + indent.appendChild(dateContainer); + if (publishContainer) { + indent.appendChild(publishContainer); + } + indent.appendChild(hiddenContainer); + + container.appendChild(box); + container.appendChild(indent); + + radiogroup.appendChild(container); + } + + // Reflect the selected key in the UI. + radiogroup.selectedItem = radiogroup.querySelector( + `radio[value="${gKeyId}"]` + ); + + // Update all the encryption options based on the selected OpenPGP key. + if (gKeyId) { + enableEncryptionControls(true); + enableSigningControls(true); + } else { + let stillHaveOtherEncryption = + gEncryptionCertName && gEncryptionCertName.value; + if (!stillHaveOtherEncryption) { + enableEncryptionControls(false); + } + let stillHaveOtherSigning = gSignCertName && gSignCertName.value; + if (!stillHaveOtherSigning) { + enableSigningControls(false); + } + } + + updateTechPref(); + enableSelectButtons(); + updateUIForSelectedOpenPgpKey(); + + gAttachKey.disabled = !gKeyId; + gEncryptSubject.disabled = !gKeyId; + gSendAutocryptHeaders.disabled = !gKeyId; +} + +/** + * Open the Key Properties subdialog. + * + * @param {string} keyId - The ID of the selected OpenPGP Key. + */ +function enigmailKeyDetails(keyId) { + keyId = keyId.replace(/^0x/, ""); + + parent.gSubDialog.open( + "chrome://openpgp/content/ui/keyDetailsDlg.xhtml", + undefined, + { + keyId, + modified: onDataModified, + } + ); +} + +/** + * Delete an OpenPGP Key. + * + * @param {object} key - The selected OpenPGP Key. + */ +async function enigmailDeleteKey(key) { + // Interrupt if the selected key is currently being used. + if (key.keyId == 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 l10nKey = key.secretAvailable ? "delete-secret-key" : "delete-pub-key"; + let [title, description] = await document.l10n.formatValues([ + { id: "delete-key-title" }, + { id: l10nKey, args: { userId: key.userId } }, + ]); + + // Ask for confirmation before proceeding. + if (!Services.prompt.confirm(null, title, description)) { + return; + } + + let cApi = EnigmailCryptoAPI(); + await cApi.deleteKey(key.fpr, key.secretAvailable); + await PgpSqliteDb2.deleteAcceptance(key.fpr); + + EnigmailKeyRing.clearCache(); + reloadOpenPgpUI(); +} + +/** + * Revoke the selected OpenPGP Key. + * + * @param {object} key - The selected OpenPGP Key. + */ +async function openPgpRevokeKey(key) { + // Interrupt if the selected key is currently being used. + if (key.keyId == gIdentity.getUnicharAttribute("openpgp_key_id")) { + let [alertTitle, alertDescription] = await document.l10n.formatValues([ + { id: "key-in-use-title" }, + { id: "revoke-key-in-use-description" }, + ]); + + Services.prompt.alert(null, alertTitle, alertDescription); + return; + } + + EnigRevokeKey(key, function (success) { + if (success) { + document.l10n.setAttributes( + document.getElementById("openPgpNotificationDescription"), + "openpgp-key-revoke-success" + ); + document.getElementById("openPgpNotification").collapsed = false; + + EnigmailKeyRing.clearCache(); + reloadOpenPgpUI(); + } + }); +} + +async function amE2eUploadKey(key) { + let ks = EnigmailKeyserverURIs.getUploadKeyServer(); + + let ok = await EnigmailKeyServer.upload(key.keyId, ks); + let msg = await document.l10n.formatValue( + ok ? "openpgp-key-publish-ok" : "openpgp-key-publish-fail", + { + keyserver: ks, + } + ); + + EnigmailDialog.alert(null, msg); +} + +/** + * Open the subdialog to enable the user to edit the expiration date of the + * selected OpenPGP Key. + * + * @param {object} key - The selected OpenPGP Key. + */ +async function enigmailEditKeyDate(key) { + if (!key.iSimpleOneSubkeySameExpiry()) { + Services.prompt.alert( + null, + document.title, + await document.l10n.formatValue("openpgp-cannot-change-expiry") + ); + return; + } + + let args = { + keyId: key.keyId, + modified: onDataModified, + }; + + parent.gSubDialog.open( + "chrome://openpgp/content/ui/changeExpiryDlg.xhtml", + undefined, + args + ); +} + +function onDataModified() { + EnigmailKeyRing.clearCache(); + reloadOpenPgpUI(); +} + +/** + * Toggle the visibility of the OpenPgp Key radio container. + * + * @param {Event} event - The DOM event. + */ +function toggleExpansion(event) { + let carat = event.target; + carat.classList.toggle("up"); + carat.closest(".content-blocking-category").classList.toggle("expanded"); + carat.setAttribute( + "aria-expanded", + carat.getAttribute("aria-expanded") === "false" + ); + event.stopPropagation(); +} + +/** + * Apply a .selected class to the radio container of the currently selected + * OpenPGP Key. + * Also update UI strings describing the status of current selection. + */ +function updateUIForSelectedOpenPgpKey() { + // Remove a previously selected container, if any. + let current = document.querySelector(".content-blocking-category.selected"); + + if (current) { + current.classList.remove("selected"); + } + + // Highlight the parent container of the currently selected radio button. + // The condition needs to be sure the key is not null as a selection of "None" + // returns a value of "". + if (gKeyId !== null) { + let radio = document.querySelector(`radio[value="${gKeyId}"]`); + + // If the currently used key was deleted, we might not have the + // corresponding radio element. + if (radio) { + radio.closest(".content-blocking-category").classList.add("selected"); + } + } + + // Reset the image in case of async reload of the list. + let statusLabel = document.getElementById("openPgpSelectionStatus"); + let image = document.getElementById("openPgpStatusImage"); + image.classList.remove("status-success", "status-error"); + + // Check if the currently selected key has expired. + if (gKeyId) { + let key = EnigmailKeyRing.getKeyById(gKeyId, true); + if (key?.expiryTime && Math.round(Date.now() / 1000) > key.expiryTime) { + image.setAttribute( + "src", + "chrome://messenger/skin/icons/new/compact/close.svg" + ); + image.classList.add("status-error"); + document.l10n.setAttributes( + statusLabel, + "openpgp-selection-status-error", + { key: `0x${gKeyId}` } + ); + } else { + image.setAttribute( + "src", + "chrome://messenger/skin/icons/new/compact/check.svg" + ); + image.classList.add("status-success"); + document.l10n.setAttributes( + statusLabel, + "openpgp-selection-status-have-key", + { key: `0x${gKeyId}` } + ); + } + } + + let hide = !gKeyId; + statusLabel.hidden = hide; + document.getElementById("openPgpLearnMore").hidden = hide; + image.hidden = hide; +} + +/** + * Generic method to copy a string in the user's clipboard. + * + * @param {string} val - The formatted string to be copied in the clipboard. + */ +async function openPgpCopyToClipboard(keyId) { + let exitCodeObj = {}; + + let keyData = await EnigmailKeyRing.extractPublicKeys( + [keyId], // full + null, + null, + null, + exitCodeObj, + {} + ); + + // Alert the user if the copy failed. + if (exitCodeObj.value !== 0) { + alertUser(await document.l10n.formatValue("copy-to-clipbrd-failed")); + return; + } + + navigator.clipboard + .writeText(keyData) + .then(async () => { + alertUser(await document.l10n.formatValue("copy-to-clipbrd-ok")); + }) + .catch(async () => { + alertUser(await document.l10n.formatValue("copy-to-clipbrd-failed")); + }); +} + +/** + * Create an attachment with the currently selected OpenPgp public Key and open + * a new message compose window. + * + * @param {string} keyId - The formatted OpenPgp Key ID. + */ +async function openPgpSendKeyEmail(keyId) { + let tmpFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + tmpFile.append("key.asc"); + tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + let exitCodeObj = {}; + let errorMsgObj = {}; + let keyIdArray = [keyId]; + + await EnigmailKeyRing.extractPublicKeys( + keyIdArray, // full + null, + null, + tmpFile, + exitCodeObj, + errorMsgObj + ); + + if (exitCodeObj.value !== 0) { + alertUser(errorMsgObj.value); + return; + } + + // Create the key attachment. + let tmpFileURI = Services.io.newFileURI(tmpFile); + let keyAttachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + + keyAttachment.url = tmpFileURI.spec; + keyAttachment.name = `${keyId}.asc`; + keyAttachment.temporary = true; + keyAttachment.contentType = "application/pgp-keys"; + + // Create the new message. + let msgCompFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + msgCompFields.addAttachment(keyAttachment); + + let msgCompParam = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + msgCompParam.composeFields = msgCompFields; + msgCompParam.identity = gIdentity; + msgCompParam.type = Ci.nsIMsgCompType.New; + msgCompParam.format = Ci.nsIMsgCompFormat.Default; + msgCompParam.originalMsgURI = ""; + + MailServices.compose.OpenComposeWindowWithParams("", msgCompParam); +} + +/** + * Export the selected OpenPGP public key to a file. + * + * @param {string} keyId - The ID of the selected OpenPGP Key. + */ +async function openPgpExportPublicKey(keyId) { + let outFile = EnigmailKeyRing.promptKeyExport2AsciiFilename( + window, + await document.l10n.formatValue("export-to-file"), + `${gIdentity.fullName}_${gIdentity.email}-${keyId}-pub.asc` + ); + + if (!outFile) { + return; + } + + let exitCodeObj = {}; + let errorMsgObj = {}; + await EnigmailKeyRing.extractPublicKeys( + [keyId], // full + null, + null, + outFile, + exitCodeObj, + errorMsgObj + ); + + // Alert the user if the save process failed. + if (exitCodeObj.value !== 0) { + document.l10n.formatValue("openpgp-export-public-fail").then(value => { + alertUser(value); + }); + return; + } + + document.l10n.setAttributes( + document.getElementById("openPgpNotificationDescription"), + "openpgp-export-public-success" + ); + document.getElementById("openPgpNotification").collapsed = false; +} + +/** + * Ask the user to pick a file location and choose a password before proceeding + * with the backup of a secret key. + * + * @param {string} keyId - The ID of the selected OpenPGP Key. + * @param {string} keyFpr - The fingerprint of the selected OpenPGP Key. + */ +async function openPgpExportSecretKey(keyId, keyFpr) { + let outFile = EnigmailKeyRing.promptKeyExport2AsciiFilename( + window, + await document.l10n.formatValue("export-keypair-to-file"), + `${gIdentity.fullName}_${gIdentity.email}-${keyId}-secret.asc` + ); + + if (!outFile) { + return; + } + + let args = { + okCallback: exportSecretKey, + file: outFile, + fprArray: [keyFpr], + }; + + window.browsingContext.topChromeWindow.openDialog( + "chrome://openpgp/content/ui/backupKeyPassword.xhtml", + "", + "dialog,modal,centerscreen,resizable", + args + ); +} + +/** + * Export the secret key after a successful password setup. + * + * @param {string} password - The declared password to protect the keys. + * @param {Array} fprArray - The array of fingerprint of the selected keys. + * @param {object} file - The file where the keys should be saved. + * @param {boolean} confirmed - If the password was properly typed in the prompt. + */ +async function exportSecretKey(password, fprArray, file, confirmed = false) { + // Interrupt in case this method has been called directly without confirming + // the input password through the password prompt. + if (!confirmed) { + return; + } + + let backupKeyBlock = await RNP.backupSecretKeys(fprArray, password); + if (!backupKeyBlock) { + Services.prompt.alert( + null, + await document.l10n.formatValue("save-keys-failed") + ); + return; + } + + await IOUtils.writeUTF8(file.path, backupKeyBlock) + .then(() => { + document.l10n.setAttributes( + document.getElementById("openPgpNotificationDescription"), + "openpgp-export-secret-success" + ); + document.getElementById("openPgpNotification").collapsed = false; + }) + .catch(async err => { + alertUser(await document.l10n.formatValue("openpgp-export-secret-fail")); + }); +} + +/** + * 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(); +} diff --git a/comm/mail/extensions/am-e2e/am-e2e.xhtml b/comm/mail/extensions/am-e2e/am-e2e.xhtml new file mode 100644 index 0000000000..8c5620dd2f --- /dev/null +++ b/comm/mail/extensions/am-e2e/am-e2e.xhtml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/am-smime.dtd"> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="parent.onPanelLoaded('am-e2e.xhtml');"> + + <script src="chrome://messenger/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://messenger/content/AccountManager.js"/> + <script src="chrome://openpgp/content/ui/enigmailCommon.js"/> + <script src="chrome://messenger/content/am-e2e.js"/> + + <vbox flex="1" style="overflow: auto;"><vbox id="containerBox" flex="1"> + <hbox class="dialogheader"> + <label class="dialogheader-title" value="&e2eTitle.label;"/> + </hbox> + + <separator class="thin"/> + +#include am-e2e.inc.xhtml + </vbox></vbox> + +</window> diff --git a/comm/mail/extensions/am-e2e/components.conf b/comm/mail/extensions/am-e2e/components.conf new file mode 100644 index 0000000000..f2399f06c2 --- /dev/null +++ b/comm/mail/extensions/am-e2e/components.conf @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{d7aad508-991c-401a-8b3f-7e4e055e1e2b}', + 'contract_ids': ['@mozilla.org/accountmanager/extension;1?name=e2e'], + 'jsm': 'resource:///modules/AME2E.jsm', + 'constructor': 'E2EService', + 'categories': {'mailnews-accountmanager-extensions': 'e2e-account-manager-extension'}, + }, +] diff --git a/comm/mail/extensions/am-e2e/moz.build b/comm/mail/extensions/am-e2e/moz.build new file mode 100644 index 0000000000..5e89391d9a --- /dev/null +++ b/comm/mail/extensions/am-e2e/moz.build @@ -0,0 +1,16 @@ +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES += [ + "AME2E.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +JS_PREFERENCE_PP_FILES += [ + "prefs/e2e-prefs.js", +] diff --git a/comm/mail/extensions/am-e2e/prefs/e2e-prefs.js b/comm/mail/extensions/am-e2e/prefs/e2e-prefs.js new file mode 100644 index 0000000000..1a88003a75 --- /dev/null +++ b/comm/mail/extensions/am-e2e/prefs/e2e-prefs.js @@ -0,0 +1,285 @@ +#filter dumbComments emptyLines substitution + +// 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/. + +// +// Prefs shared by OpenPGP and S/MIME +// + +pref("mail.identity.default.encryptionpolicy", 0); +pref("mail.identity.default.sign_mail", false); + +// +// S/MIME prefs +// + +pref("mail.identity.default.encryption_cert_name", ""); +pref("mail.identity.default.signing_cert_name", ""); + +// +// OpenPGP prefs +// + +pref("openpgp.loglevel", "Warn"); + +// 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 using external GnuPG, also load public keys from GnuPG keyring +pref("mail.openpgp.fetch_pubkeys_from_gnupg", false); + +// When sending an OpenPGP message that is both signed and encrypted, +// it's possible to use one combined MIME layer, or separate layers. +pref("mail.openpgp.separate_mime_layers", false); + +// Load a JSON file that contains recipient key alias rules. See bug 1644085. +// Suggested filename: openpgp-alias-rules.json +// Simple filenames (without path) are loaded from the profile directory. +// If you need to specify a path, use a file:// URL +pref("mail.openpgp.alias_rules_file", ""); + +pref("mail.openpgp.key_assistant.enable", true); + +// If set to true, enable user interface that allows the user to optionally set +// and manage individual, user-defined passphrases for OpenPGP secret keys. +// If set to false, the respective user interface will be hidden. +// Even when set to true, the user may decide to use the original approach +// for OpenPGP key protection (using the global primary password or none), +// by selecting the respective choices in the user interface. +// Note, if a user sets an user-defined passphrase while this this setting +// is true, and then switches this setting to false, the keys will keep +// the user-defined passphrase protection. The application will still prompt +// to unlock the key using the user-defined passphrase whenever necessary. +pref("mail.openpgp.passphrases.enabled", false); + +// Automatically enable encryption if S/MIME certificates or OpenPGP keys are +// available for all recipients, and thus encryption is possible. +// This pref is only about enabling, and doesn't control automatic disabling. +pref("mail.e2ee.auto_enable", false); + +// If end-to-end encryption with S/MIME or OpenPGP is enabled, +// and the user adds another recipient with unavailable certificate or key, +// and this preference is true, then automatically disable encryption. +// This pref is dangerous, and it is recommended to always keep it at false. +// If you change the pref to true, the user might assume that encryption +// is still enabled, and might not notice that encryption gets disabled. +// There is an exception: If encryption was enabled, because the message +// refers to an existing encrypted conversation (e.g. replying to an +// encrypted message), this preference is ignored, encryption will +// remain on. It isn't possible to override that behavior. +// Note that encryption will never be disabled automatically on sending, +// only when the list of recipients is changed. +// If mail.e2ee.auto_enable is false, then mail.e2ee.auto_disable +// will be ignored. +pref("mail.e2ee.auto_disable", false); + +// If end-to-end encryption gets automatically disabled, inform the user +// using a prompt. +pref("mail.e2ee.notify_on_auto_disable", true); + +// If false, disable the reminder in composer, whether email could be +// sent with OpenPGP encryption (without further user actions/decisions). +pref("mail.openpgp.remind_encryption_possible", true); + +// If false, disable the reminder in composer, whether email could be +// sent with S/MIME encryption (without further user actions/decisions). +pref("mail.smime.remind_encryption_possible", true); + +pref("mail.smime.accept_insecure_sha1_message_signatures", false); + +// 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: +// use rules to assign keys +pref("temp.openpgp.assignKeysByRules", true); +// use email addresses to assign keys +pref("temp.openpgp.assignKeysByEmailAddr", true); +// use manual dialog to assign missing keys +pref("temp.openpgp.assignKeysManuallyIfMissing", true); +// always srats manual dialog for keys +pref("temp.openpgp.assignKeysManuallyAlways", false); + +// enable automatically decrypt/verify +pref("temp.openpgp.autoDecrypt", true); + +// countdown for alerts when composing inline PGP HTML msgs +pref("temp.openpgp.composeHtmlAlertCount", 3); + +// show warning message when clicking on sign icon +pref("temp.openpgp.displaySignWarn", true); + +// try to match secondary uid to from address +pref("temp.openpgp.displaySecondaryUid", true); + +// treat '-- ' as signature separator +pref("temp.openpgp.doubleDashSeparator", true); + +// skip the attachments dialog +pref("temp.openpgp.encryptAttachmentsSkipDlg", 0); + +// Encrypt to self +pref("temp.openpgp.encryptToSelf", true); + +// enable 'Decrypt & open' for double click on attachment (if possible) +pref("temp.openpgp.handleDoubleClick", true); + +// disable '<' and '>' around email addresses +pref("temp.openpgp.hushMailSupport", false); + +// use -a for encrypting attachments for inline PGP +pref("temp.openpgp.inlineAttachAsciiArmor", false); + +// extension to append for inline-encrypted attachments +pref("temp.openpgp.inlineAttachExt", ".pgp"); + +// extension to append for inline-signed attachments +pref("temp.openpgp.inlineSigAttachExt", ".sig"); + +// debug log directory (if set, also enabled debugging) +pref("temp.openpgp.logDirectory", ""); + +// List of key servers to use (comma separated list), ordered by priority. +// Only the first supported keyserver will be used for uploading keys. +pref("mail.openpgp.keyserver_list", "vks://keys.openpgp.org, hkps://keys.mailvelope.com"); + +// keep passphrase for ... minutes +pref("temp.openpgp.maxIdleMinutes", 5); + +// maximum number of parallel decrypt processes that Enigmaik will handle +// (requests above the threshold are ignored) +pref("temp.openpgp.maxNumProcesses", 3); + +// GnuPG hash algorithm +// 0: automatic seletion (i.e. let GnuPG choose) +// 1: SHA1, 2: RIPEMD160, 3: SHA256, 4: SHA384, 5: SHA512, 6: SHA224 +pref("temp.openpgp.mimeHashAlgorithm", 0); + +// no passphrase for GnuPG key needed +pref("temp.openpgp.noPassphrase", false); + +// show quoted printable warning message (and remember selected state) +pref("temp.openpgp.quotedPrintableWarn", 0); + +// use http proxy settings as set in Mozilla/Thunderbird +pref("temp.openpgp.respectHttpProxy", true); + +// selection for which encryption model to prefer +// 0: convenient encryption settings DEFAULT +// 1: manual encryption settings +pref("temp.openpgp.encryptionModel", 0); + +// enable encryption for replies to encrypted mails +pref("temp.openpgp.keepSettingsForReply", true); + +// holds the last result of the dayily key expiry check +pref("temp.openpgp.keyCheckResult", ""); + +// selection for automatic send encrypted if all keys valid +// 0: never +// 1: if all keys found and accepted DEFAULT +pref("temp.openpgp.autoSendEncrypted", 1); + +// enable automatic lookup of keys using Web Key Directory (WKD) +// (see https://tools.ietf.org/html/draft-koch-openpgp-webkey-service) +// 0: no +// 1: yes DEFAULT +pref("temp.openpgp.autoWkdLookup", 1); + +// ask to confirm before sending +// 0: never DEFAULT +// 1: always +// 2: if send encrypted +// 3: if send unencrypted +// 4: if send (un)encrypted due to rules +pref("temp.openpgp.confirmBeforeSending", 0); + +// show "Missing Trust in own keys" message (and remember selected state) +pref("temp.openpgp.warnOnMissingOwnerTrust", true); + +// use GnuPG's default instead of Enigmail/Mozilla comment of for signed messages +pref("temp.openpgp.useDefaultComment", true); + +// allow encryption to newsgroups +pref("temp.openpgp.encryptToNews", false); +pref("temp.openpgp.warnOnSendingNewsgroups", true); + +// holds the timestamp of the last check for GnuPG updates +pref("temp.openpgp.gpgLastUpdate", "0"); + +// set locale for GnuPG calls to en-US (Windows only) +pref("temp.openpgp.gpgLocaleEn", true); + +// use PGP/MIME (0=never, 1=allow, 2=always) +// pref("temp.openpgp.usePGPMimeOption",1); -- OBSOLETE, see mail.identity.default.pgpMimeMode + +// show "conflicting rules" message (and remember selected state) +pref("temp.openpgp.warnOnRulesConflict", 0); + +// display a warning when the passphrase is cleared +pref("temp.openpgp.warnClearPassphrase", true); + +// display a warning if the GnuPG version is deprecated +pref("temp.openpgp.warnDeprecatedGnuPG", true); + +// warn if gpg-agent is found and "remember passphrase for X minutes is active" +pref("temp.openpgp.warnGpgAgentAndIdleTime", true); + +// display a warning when the keys for all contacts are downloaded +pref("temp.openpgp.warnDownloadContactKeys", true); + +// wrap HTML messages before sending inline PGP messages +pref("temp.openpgp.wrapHtmlBeforeSend", true); + +// do reset the "references" and "in-reply-to" headers? +pref("temp.openpgp.protectReferencesHdr", false); + +// tor configuration +pref("temp.openpgp.torIpAddr", "127.0.0.1"); +pref("temp.openpgp.torServicePort", "9050"); +pref("temp.openpgp.torBrowserBundlePort", "9150"); + +// gpg tor actions +pref("temp.openpgp.downloadKeyWithTor", false); +pref("temp.openpgp.downloadKeyRequireTor", false); +pref("temp.openpgp.searchKeyWithTor", false); +pref("temp.openpgp.searchKeyRequireTor", false); +pref("temp.openpgp.uploadKeyWithTor", false); +pref("temp.openpgp.uploadKeyRequireTor", false); + +// enable experimental features. +// WARNING: such features may unfinished functions or tests that can break +// existing functionality in Enigmail and Thunderbird! +pref("temp.openpgp.enableExperiments", false); + + +// Default pref values for the enigmail per-identity +// settings + +pref("mail.identity.default.sendAutocryptHeaders", true); +pref("mail.identity.default.attachPgpKey", true); +pref("mail.identity.default.autoEncryptDrafts", true); +pref("mail.identity.default.protectSubject", true); + +// 0 selected automatically, 1 prefer S/MIME, 2 prefer OpenPGP +pref("mail.identity.default.e2etechpref", 0); + +// +// Other settings (change Mozilla behaviour) +// + +// disable flowed text by default +// TODO: pref("mailnews.send_plaintext_flowed", false); + |