diff options
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm')
-rw-r--r-- | comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm | 1338 |
1 files changed, 1338 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm b/comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm new file mode 100644 index 0000000000..17de2e3246 --- /dev/null +++ b/comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm @@ -0,0 +1,1338 @@ +/* + * 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 https://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var EXPORTED_SYMBOLS = ["EnigmailPersistentCrypto"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm", + EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm", + EnigmailData: "chrome://openpgp/content/modules/data.jsm", + EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm", + EnigmailEncryption: "chrome://openpgp/content/modules/encryption.jsm", + EnigmailLog: "chrome://openpgp/content/modules/log.jsm", + EnigmailMime: "chrome://openpgp/content/modules/mime.jsm", + EnigmailFixExchangeMsg: + "chrome://openpgp/content/modules/fixExchangeMessage.jsm", + EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm", + EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm", + GlodaUtils: "resource:///modules/gloda/GlodaUtils.jsm", + jsmime: "resource:///modules/jsmime.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", + MailCryptoUtils: "resource:///modules/MailCryptoUtils.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "l10n", () => { + return new Localization(["messenger/openpgp/openpgp.ftl"], true); +}); + +var EnigmailPersistentCrypto = { + /*** + * cryptMessage + * + * Decrypts a message and copy it to a folder. If targetKey is + * not null, it encrypts a message to the target key afterwards. + * + * @param {nsIMsgDBHdr} hdr - message to process + * @param {string} destFolder - target folder URI + * @param {boolean} move - true for move, false for copy + * @param {KeyObject} targetKey - target key if encryption is requested + * + * @returns {nsMsgKey} Message key of the new message + **/ + async cryptMessage(hdr, destFolder, move, targetKey) { + return new Promise(function (resolve, reject) { + let msgUriSpec = hdr.folder.getUriForMsg(hdr); + let msgUrl = lazy.EnigmailFuncs.getUrlFromUriSpec(msgUriSpec); + + const crypt = new CryptMessageIntoFolder(destFolder, move, targetKey); + + lazy.EnigmailMime.getMimeTreeFromUrl(msgUrl, true, async function (mime) { + try { + let newMsgKey = await crypt.messageParseCallback(mime, hdr); + resolve(newMsgKey); + } catch (ex) { + reject(ex); + } + }); + }); + }, + + changeMessageId(content, newMessageIdPrefix) { + let [headerData, body] = MimeParser.extractHeadersAndBody(content); + content = ""; + + let newHeaders = headerData.rawHeaderText; + if (!newHeaders.endsWith("\r\n")) { + newHeaders += "\r\n"; + } + + headerData = undefined; + + let regExpMsgId = new RegExp("^message-id: <(.*)>", "mi"); + let msgId; + let match = newHeaders.match(regExpMsgId); + + if (match) { + msgId = match[1]; + newHeaders = newHeaders.replace( + regExpMsgId, + "Message-Id: <" + newMessageIdPrefix + "-$1>" + ); + + // Match the references header across multiple lines + // eslint-disable-next-line no-control-regex + let regExpReferences = new RegExp("^references: .*([\r\n]*^ .*$)*", "mi"); + let refLines = newHeaders.match(regExpReferences); + if (refLines) { + // Take the full match of the existing header + let newRef = refLines[0] + " <" + msgId + ">"; + newHeaders = newHeaders.replace(regExpReferences, newRef); + } else { + newHeaders += "References: <" + msgId + ">\r\n"; + } + } + + return newHeaders + "\r\n" + body; + }, + + /* + * Copies an email message to a folder, which is a modified copy of an + * existing message, optionally creating a new message ID. + * + * @param {nsIMsgDBHdr} originalMsgHdr - Header of the original message + * @param {string} targetFolderUri - Target folder URI + * @param {boolean} deleteOrigMsg - Should the original message be deleted? + * @param {string} content - New message content + * @param {string} newMessageIdPrefix - If this is non-null, create a new message ID + * by adding this prefix. + * + * @returns {nsMsgKey} Message key of the new message + */ + async copyMessageToFolder( + originalMsgHdr, + targetFolderUri, + deleteOrigMsg, + content, + newMessageIdPrefix + ) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: copyMessageToFolder()\n"); + return new Promise((resolve, reject) => { + if (newMessageIdPrefix) { + content = this.changeMessageId(content, newMessageIdPrefix); + } + + // Create the temporary file where the new message will be stored. + let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + tempFile.append("message.eml"); + tempFile.createUnique(0, 0o600); + + let outputStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outputStream.init(tempFile, 2, 0x200, false); // open as "write only" + outputStream.write(content, content.length); + outputStream.close(); + + // Delete file on exit, because Windows locks the file + let extAppLauncher = Cc[ + "@mozilla.org/uriloader/external-helper-app-service;1" + ].getService(Ci.nsPIExternalAppLauncher); + extAppLauncher.deleteTemporaryFileOnExit(tempFile); + + let msgFolder = originalMsgHdr.folder; + + // The following technique was copied from AttachmentDeleter in Thunderbird's + // nsMessenger.cpp. There is a "unified" listener which serves as copy and delete + // listener. In all cases, the `OnStopCopy()` of the delete listener selects the + // replacement message. + // The deletion happens in `OnStopCopy()` of the copy listener for local messages + // and in `OnStopRunningUrl()` for IMAP messages if the folder is displayed since + // otherwise `OnStopRunningUrl()` doesn't run. + + let copyListener, newKey; + let statusCode = 0; + let destFolder = targetFolderUri + ? lazy.MailUtils.getExistingFolder(targetFolderUri) + : msgFolder; + + copyListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgCopyServiceListener", + "nsIUrlListener", + ]), + GetMessageId(messageId) { + // Maybe enable this later. Most of the Thunderbird code does not supply this. + // messageId = { value: msgHdr.messageId }; + }, + SetMessageKey(key) { + lazy.EnigmailLog.DEBUG( + `persistentCrypto.jsm: copyMessageToFolder: Result of CopyFileMessage() is new message with key ${key}\n` + ); + newKey = key; + }, + applyFlags() { + let newHdr = destFolder.GetMessageHeader(newKey); + newHdr.markRead(originalMsgHdr.isRead); + newHdr.markFlagged(originalMsgHdr.isFlagged); + newHdr.subject = originalMsgHdr.subject; + }, + OnStartCopy() {}, + OnStopCopy(status) { + statusCode = status; + if (statusCode !== 0) { + lazy.EnigmailLog.ERROR( + `persistentCrypto.jsm: ${statusCode} replacing message, folder="${msgFolder.name}", key=${originalMsgHdr.messageKey}/${newKey}\n` + ); + reject(); + return; + } + + try { + tempFile.remove(); + } catch (ex) {} + + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: copyMessageToFolder: Triggering deletion from OnStopCopy()\n" + ); + this.applyFlags(); + + if (deleteOrigMsg) { + lazy.EnigmailLog.DEBUG( + `persistentCrypto.jsm: copyMessageToFolder: Deleting old message with key ${originalMsgHdr.messageKey}\n` + ); + msgFolder.deleteMessages( + [originalMsgHdr], + null, + true, + false, + null, + false + ); + } + resolve(newKey); + }, + }; + + MailServices.copy.copyFileMessage( + tempFile, + destFolder, + null, + false, + originalMsgHdr.flags, + "", + copyListener, + null + ); + }); + }, +}; + +function CryptMessageIntoFolder(destFolder, move, targetKey) { + this.destFolder = destFolder; + this.move = move; + this.targetKey = targetKey; + this.cryptoChanged = false; + this.decryptFailure = false; + + this.mimeTree = null; + this.decryptionTasks = []; + this.subject = ""; +} + +CryptMessageIntoFolder.prototype = { + /** Here is the effective action of a call to cryptMessage. + * If no failure is seen when attempting to decrypt (!decryptFailure), + * then we copy. (This includes plain messages that didn't need + * decryption.) + * The cryptoChanged flag is set only after we have successfully + * completed a decryption (or encryption) operation, it's used to + * decide whether we need a new message ID. + */ + async messageParseCallback(mimeTree, msgHdr) { + this.mimeTree = mimeTree; + this.hdr = msgHdr; + + if (mimeTree.headers.has("subject")) { + this.subject = mimeTree.headers.get("subject"); + } + + await this.decryptMimeTree(mimeTree); + + let msg = ""; + + // Encrypt the message if a target key is given. + if (this.targetKey) { + msg = this.encryptToKey(mimeTree); + if (!msg) { + throw new Error("Failure to encrypt message"); + } + this.cryptoChanged = true; + } else { + msg = this.mimeToString(mimeTree, true); + } + + if (this.decryptFailure) { + throw new Error("Failure to decrypt message"); + } + return EnigmailPersistentCrypto.copyMessageToFolder( + this.hdr, + this.destFolder, + this.move, + msg, + this.cryptoChanged ? "decrypted-" + new Date().valueOf() : null + ); + }, + + encryptToKey(mimeTree) { + let exitCodeObj = {}; + let statusFlagsObj = {}; + let errorMsgObj = {}; + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: Encrypting message.\n"); + + let inputMsg = this.mimeToString(mimeTree, false); + + let encmsg = ""; + try { + encmsg = lazy.EnigmailEncryption.encryptMessage( + null, + 0, + inputMsg, + "0x" + this.targetKey.fpr, + "0x" + this.targetKey.fpr, + "", + lazy.EnigmailConstants.SEND_ENCRYPTED | + lazy.EnigmailConstants.SEND_ALWAYS_TRUST, + exitCodeObj, + statusFlagsObj, + errorMsgObj + ); + } catch (ex) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: Encryption failed: " + ex + "\n" + ); + return null; + } + + // Build the pgp-encrypted mime structure + let msg = ""; + + let rfc822Headers = []; // FIXME + + // First the original headers + for (let header in rfc822Headers) { + if ( + header != "content-type" && + header != "content-transfer-encoding" && + header != "content-disposition" + ) { + msg += prettyPrintHeader(header, rfc822Headers[header]) + "\n"; + } + } + // Then multipart/encrypted ct + let boundary = lazy.EnigmailMime.createBoundary(); + msg += "Content-Transfer-Encoding: 7Bit\n"; + msg += "Content-Type: multipart/encrypted; "; + msg += + 'boundary="' + boundary + '"; protocol="application/pgp-encrypted"\n\n'; + msg += "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n"; + + // pgp-encrypted part + msg += "--" + boundary + "\n"; + msg += "Content-Type: application/pgp-encrypted\n"; + msg += "Content-Disposition: attachment\n"; + msg += "Content-Transfer-Encoding: 7Bit\n\n"; + msg += "Version: 1\n\n"; + + // the octet stream + msg += "--" + boundary + "\n"; + msg += 'Content-Type: application/octet-stream; name="encrypted.asc"\n'; + msg += "Content-Description: OpenPGP encrypted message\n"; + msg += 'Content-Disposition: inline; filename="encrypted.asc"\n'; + msg += "Content-Transfer-Encoding: 7Bit\n\n"; + msg += encmsg; + + // Bottom boundary + msg += "\n--" + boundary + "--\n"; + + // Fix up the line endings to be a proper dosish mail + msg = msg.replace(/\r/gi, "").replace(/\n/gi, "\r\n"); + + return msg; + }, + + /** + * Walk through the MIME message structure and decrypt the body if there is something to decrypt + */ + async decryptMimeTree(mimePart) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: decryptMimeTree:\n"); + + if (this.isBrokenByExchange(mimePart)) { + this.fixExchangeMessage(mimePart); + } + + if (this.isSMIME(mimePart)) { + this.decryptSMIME(mimePart); + } else if (this.isPgpMime(mimePart)) { + this.decryptPGPMIME(mimePart); + } else if (isAttachment(mimePart)) { + this.pgpDecryptAttachment(mimePart); + } else { + this.decryptINLINE(mimePart); + } + + for (let i in mimePart.subParts) { + await this.decryptMimeTree(mimePart.subParts[i]); + } + }, + + /*** + * + * Detect if mime part is PGP/MIME message that got modified by MS-Exchange: + * + * - multipart/mixed Container with + * - application/pgp-encrypted Attachment with name "PGPMIME Version Identification" + * - application/octet-stream Attachment with name "encrypted.asc" having the encrypted content in base64 + * - see: + * - https://doesnotexist-openpgp-integration.thunderbird/forum/viewtopic.php?f=4&t=425 + * - https://sourceforge.net/p/enigmail/forum/support/thread/4add2b69/ + */ + + isBrokenByExchange(mime) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: isBrokenByExchange:\n"); + + try { + if ( + mime.subParts && + mime.subParts.length === 3 && + mime.fullContentType.toLowerCase().includes("multipart/mixed") && + mime.subParts[0].subParts.length === 0 && + mime.subParts[0].fullContentType.search(/multipart\/encrypted/i) < 0 && + mime.subParts[0].fullContentType.toLowerCase().includes("text/plain") && + mime.subParts[1].fullContentType + .toLowerCase() + .includes("application/pgp-encrypted") && + mime.subParts[1].fullContentType + .toLowerCase() + .search(/multipart\/encrypted/i) < 0 && + mime.subParts[1].fullContentType + .toLowerCase() + .search(/PGPMIME Versions? Identification/i) >= 0 && + mime.subParts[2].fullContentType + .toLowerCase() + .includes("application/octet-stream") && + mime.subParts[2].fullContentType.toLowerCase().includes("encrypted.asc") + ) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: isBrokenByExchange: found message broken by MS-Exchange\n" + ); + return true; + } + } catch (ex) {} + + return false; + }, + + decryptSMIME(mimePart) { + let encrypted = lazy.MailCryptoUtils.binaryStringToTypedArray( + mimePart.body + ); + + let cmsDecoderJS = Cc["@mozilla.org/nsCMSDecoderJS;1"].createInstance( + Ci.nsICMSDecoderJS + ); + let decrypted = cmsDecoderJS.decrypt(encrypted); + + if (decrypted.length === 0) { + // fail if no data found + this.decryptFailure = true; + return; + } + + let data = ""; + for (let c of decrypted) { + data += String.fromCharCode(c); + } + + if (lazy.EnigmailLog.getLogLevel() > 5) { + lazy.EnigmailLog.DEBUG( + "*** start data ***\n'" + data + "'\n***end data***\n" + ); + } + + // Search for the separator between headers and message body. + let bodyIndex = data.search(/\n\s*\r?\n/); + if (bodyIndex < 0) { + // not found, body starts at beginning. + bodyIndex = 0; + } else { + // found, body starts after the headers. + let wsSize = data.match(/\n\s*\r?\n/); + bodyIndex += wsSize[0].length; + } + + if (data.substr(bodyIndex).search(/\r?\n$/) === 0) { + return; + } + + let m = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + // headers are found from the beginning up to the start of the body + m.initialize(data.substr(0, bodyIndex)); + + mimePart.headers._rawHeaders.set("content-type", [ + m.extractHeader("content-type", false) || "", + ]); + + mimePart.headers._rawHeaders.delete("content-transfer-encoding"); + mimePart.headers._rawHeaders.delete("content-disposition"); + mimePart.headers._rawHeaders.delete("content-description"); + + mimePart.subParts = []; + mimePart.body = data.substr(bodyIndex); + + this.cryptoChanged = true; + }, + + isSMIME(mimePart) { + if (!mimePart.headers.has("content-type")) { + return false; + } + + return ( + mimePart.headers.get("content-type").type.toLowerCase() === + "application/pkcs7-mime" && + mimePart.headers.get("content-type").get("smime-type").toLowerCase() === + "enveloped-data" && + mimePart.subParts.length === 0 + ); + }, + + isPgpMime(mimePart) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: isPgpMime()\n"); + + try { + if (mimePart.headers.has("content-type")) { + if ( + mimePart.headers.get("content-type").type.toLowerCase() === + "multipart/encrypted" && + mimePart.headers.get("content-type").get("protocol").toLowerCase() === + "application/pgp-encrypted" && + mimePart.subParts.length === 2 + ) { + return true; + } + } + } catch (x) {} + return false; + }, + + async decryptPGPMIME(mimePart) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: decryptPGPMIME(" + mimePart.partNum + ")\n" + ); + + if (!mimePart.subParts[1]) { + throw new Error("Not a correct PGP/MIME message"); + } + + const uiFlags = + lazy.EnigmailConstants.UI_INTERACTIVE | + lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK | + lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR; + let exitCodeObj = {}; + let statusFlagsObj = {}; + let userIdObj = {}; + let sigDetailsObj = {}; + let errorMsgObj = {}; + let keyIdObj = {}; + let blockSeparationObj = { + value: "", + }; + let encToDetailsObj = {}; + var signatureObj = {}; + signatureObj.value = ""; + + let data = lazy.EnigmailDecryption.decryptMessage( + null, + uiFlags, + mimePart.subParts[1].body, + null, // date + signatureObj, + exitCodeObj, + statusFlagsObj, + keyIdObj, + userIdObj, + sigDetailsObj, + errorMsgObj, + blockSeparationObj, + encToDetailsObj + ); + + if (!data || data.length === 0) { + if (statusFlagsObj.value & lazy.EnigmailConstants.DISPLAY_MESSAGE) { + lazy.EnigmailDialog.alert(null, errorMsgObj.value); + throw new Error("Decryption impossible"); + } + } + + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: analyzeDecryptedData: got " + + data.length + + " bytes\n" + ); + + if (lazy.EnigmailLog.getLogLevel() > 5) { + lazy.EnigmailLog.DEBUG( + "*** start data ***\n'" + data + "'\n***end data***\n" + ); + } + + if (data.length === 0) { + // fail if no data found + this.decryptFailure = true; + return; + } + + let bodyIndex = data.search(/\n\s*\r?\n/); + if (bodyIndex < 0) { + bodyIndex = 0; + } else { + let wsSize = data.match(/\n\s*\r?\n/); + bodyIndex += wsSize[0].length; + } + + if (data.substr(bodyIndex).search(/\r?\n$/) === 0) { + return; + } + + let m = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + m.initialize(data.substr(0, bodyIndex)); + let ct = m.extractHeader("content-type", false) || ""; + let part = mimePart.partNum; + + if (part.length > 0 && part.search(/[^01.]/) < 0) { + if (ct.search(/protected-headers/i) >= 0) { + if (m.hasHeader("subject")) { + let subject = m.extractHeader("subject", false) || ""; + subject = subject.replace(/^(Re: )+/, "Re: "); + this.mimeTree.headers._rawHeaders.set("subject", [subject]); + } + } else if (this.mimeTree.headers.get("subject") === "p≡p") { + let subject = getPepSubject(data); + if (subject) { + subject = subject.replace(/^(Re: )+/, "Re: "); + this.mimeTree.headers._rawHeaders.set("subject", [subject]); + } + } else if ( + !(statusFlagsObj.value & lazy.EnigmailConstants.GOOD_SIGNATURE) && + /^multipart\/signed/i.test(ct) + ) { + // RFC 3156, Section 6.1 message + let innerMsg = lazy.EnigmailMime.getMimeTree(data, false); + if (innerMsg.subParts.length > 0) { + ct = innerMsg.subParts[0].fullContentType; + let hdrMap = innerMsg.subParts[0].headers._rawHeaders; + if (ct.search(/protected-headers/i) >= 0 && hdrMap.has("subject")) { + let subject = innerMsg.subParts[0].headers._rawHeaders + .get("subject") + .join(""); + subject = subject.replace(/^(Re: )+/, "Re: "); + this.mimeTree.headers._rawHeaders.set("subject", [subject]); + } + } + } + } + + let boundary = getBoundary(mimePart); + if (!boundary) { + boundary = lazy.EnigmailMime.createBoundary(); + } + + // append relevant headers + mimePart.headers.get("content-type").type = "multipart/mixed"; + mimePart.headers._rawHeaders.set("content-type", [ + 'multipart/mixed; boundary="' + boundary + '"', + ]); + mimePart.subParts = [ + { + body: data, + decryptedPgpMime: true, + partNum: mimePart.partNum + ".1", + headers: { + _rawHeaders: new Map(), + get() { + return null; + }, + has() { + return false; + }, + }, + subParts: [], + }, + ]; + + this.cryptoChanged = true; + }, + + pgpDecryptAttachment(mimePart) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: pgpDecryptAttachment()\n"); + let attachmentHead = mimePart.body.substr(0, 30); + if (attachmentHead.search(/-----BEGIN PGP \w{5,10} KEY BLOCK-----/) >= 0) { + // attachment appears to be a PGP key file, skip + return; + } + + const uiFlags = + lazy.EnigmailConstants.UI_INTERACTIVE | + lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK | + lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR; + let exitCodeObj = {}; + let statusFlagsObj = {}; + let userIdObj = {}; + let sigDetailsObj = {}; + let errorMsgObj = {}; + let keyIdObj = {}; + let blockSeparationObj = { + value: "", + }; + let encToDetailsObj = {}; + var signatureObj = {}; + signatureObj.value = ""; + + let attachmentName = getAttachmentName(mimePart); + attachmentName = attachmentName + ? attachmentName.replace(/\.(pgp|asc|gpg)$/, "") + : ""; + + let data = lazy.EnigmailDecryption.decryptMessage( + null, + uiFlags, + mimePart.body, + null, // date + signatureObj, + exitCodeObj, + statusFlagsObj, + keyIdObj, + userIdObj, + sigDetailsObj, + errorMsgObj, + blockSeparationObj, + encToDetailsObj + ); + + if (data || statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_OKAY) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: pgpDecryptAttachment: decryption OK\n" + ); + } else if ( + statusFlagsObj.value & + (lazy.EnigmailConstants.DECRYPTION_FAILED | + lazy.EnigmailConstants.MISSING_MDC) + ) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: pgpDecryptAttachment: decryption without MDC protection\n" + ); + } else if ( + statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_FAILED + ) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: pgpDecryptAttachment: decryption failed\n" + ); + // Enigmail prompts the user here, but we just keep going. + } else if ( + statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_INCOMPLETE + ) { + // failure; message not complete + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: pgpDecryptAttachment: decryption incomplete\n" + ); + return; + } else { + // there is nothing to be decrypted + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: pgpDecryptAttachment: no decryption required\n" + ); + return; + } + + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: pgpDecryptAttachment: decrypted to " + + data.length + + " bytes\n" + ); + if (statusFlagsObj.encryptedFileName) { + attachmentName = statusFlagsObj.encryptedFileName; + } + + this.decryptedMessage = true; + mimePart.body = data; + mimePart.headers._rawHeaders.set( + "content-disposition", + `attachment; filename="${attachmentName}"` + ); + mimePart.headers._rawHeaders.set("content-transfer-encoding", ["base64"]); + let origCt = mimePart.headers.get("content-type"); + let ct = origCt.type; + + for (let i of origCt.entries()) { + if (i[0].toLowerCase() === "name") { + i[1] = i[1].replace(/\.(pgp|asc|gpg)$/, ""); + } + ct += `; ${i[0]}="${i[1]}"`; + } + + mimePart.headers._rawHeaders.set("content-type", [ct]); + }, + + async decryptINLINE(mimePart) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: decryptINLINE()\n"); + + if ("decryptedPgpMime" in mimePart && mimePart.decryptedPgpMime) { + return 0; + } + + if ("body" in mimePart && mimePart.body.length > 0) { + let ct = getContentType(mimePart); + + if (ct === "text/html") { + mimePart.body = this.stripHTMLFromArmoredBlocks(mimePart.body); + } + + var exitCodeObj = {}; + var statusFlagsObj = {}; + var userIdObj = {}; + var sigDetailsObj = {}; + var errorMsgObj = {}; + var keyIdObj = {}; + var blockSeparationObj = { + value: "", + }; + var encToDetailsObj = {}; + var signatureObj = {}; + signatureObj.value = ""; + + const uiFlags = + lazy.EnigmailConstants.UI_INTERACTIVE | + lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK | + lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR; + + var plaintexts = []; + var blocks = lazy.EnigmailArmor.locateArmoredBlocks(mimePart.body); + var tmp = []; + + for (let i = 0; i < blocks.length; i++) { + if (blocks[i].blocktype == "MESSAGE") { + tmp.push(blocks[i]); + } + } + + blocks = tmp; + + if (blocks.length < 1) { + return 0; + } + + let charset = "utf-8"; + + for (let i = 0; i < blocks.length; i++) { + let plaintext = null; + do { + let ciphertext = mimePart.body.substring( + blocks[i].begin, + blocks[i].end + 1 + ); + + if (ciphertext.length === 0) { + break; + } + + let hdr = ciphertext.search(/(\r\r|\n\n|\r\n\r\n)/); + if (hdr > 0) { + let chset = ciphertext.substr(0, hdr).match(/^(charset:)(.*)$/im); + if (chset && chset.length == 3) { + charset = chset[2].trim(); + } + } + plaintext = lazy.EnigmailDecryption.decryptMessage( + null, + uiFlags, + ciphertext, + null, // date + signatureObj, + exitCodeObj, + statusFlagsObj, + keyIdObj, + userIdObj, + sigDetailsObj, + errorMsgObj, + blockSeparationObj, + encToDetailsObj + ); + if (!plaintext || plaintext.length === 0) { + if (statusFlagsObj.value & lazy.EnigmailConstants.DISPLAY_MESSAGE) { + lazy.EnigmailDialog.alert(null, errorMsgObj.value); + this.cryptoChanged = false; + this.decryptFailure = true; + return -1; + } + + if ( + statusFlagsObj.value & + (lazy.EnigmailConstants.DECRYPTION_FAILED | + lazy.EnigmailConstants.MISSING_MDC) + ) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: decryptINLINE: no MDC protection, decrypting anyway\n" + ); + } + if ( + statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_FAILED + ) { + // since we cannot find out if the user wants to cancel + // we should ask + let msg = await lazy.l10n.formatValue( + "converter-decrypt-body-failed", + { + subject: this.subject, + } + ); + + if ( + !lazy.EnigmailDialog.confirmDlg( + null, + msg, + lazy.l10n.formatValueSync("dlg-button-retry"), + lazy.l10n.formatValueSync("dlg-button-skip") + ) + ) { + this.cryptoChanged = false; + this.decryptFailure = true; + return -1; + } + } else if ( + statusFlagsObj.value & + lazy.EnigmailConstants.DECRYPTION_INCOMPLETE + ) { + this.cryptoChanged = false; + this.decryptFailure = true; + return -1; + } else { + plaintext = " "; + } + } + + if (ct === "text/html") { + plaintext = plaintext.replace(/\n/gi, "<br/>\n"); + } + + let subject = ""; + if (this.mimeTree.headers.has("subject")) { + subject = this.mimeTree.headers.get("subject"); + } + + if ( + i == 0 && + subject === "pEp" && + mimePart.partNum.length > 0 && + mimePart.partNum.search(/[^01.]/) < 0 + ) { + let m = lazy.EnigmailMime.extractSubjectFromBody(plaintext); + if (m) { + plaintext = m.messageBody; + this.mimeTree.headers._rawHeaders.set("subject", [m.subject]); + } + } + + if (plaintext) { + plaintexts.push(plaintext); + } + } while (!plaintext || plaintext === ""); + } + + var decryptedMessage = + mimePart.body.substring(0, blocks[0].begin) + plaintexts[0]; + for (let i = 1; i < blocks.length; i++) { + decryptedMessage += + mimePart.body.substring(blocks[i - 1].end + 1, blocks[i].begin + 1) + + plaintexts[i]; + } + + decryptedMessage += mimePart.body.substring( + blocks[blocks.length - 1].end + 1 + ); + + // enable base64 encoding if non-ASCII character(s) found + let j = decryptedMessage.search(/[^\x01-\x7F]/); // eslint-disable-line no-control-regex + if (j >= 0) { + mimePart.headers._rawHeaders.set("content-transfer-encoding", [ + "base64", + ]); + } else { + mimePart.headers._rawHeaders.set("content-transfer-encoding", ["8bit"]); + } + mimePart.body = decryptedMessage; + + let origCharset = getCharset(mimePart, "content-type"); + if (origCharset) { + mimePart.headers_rawHeaders.set( + "content-type", + getHeaderValue(mimePart, "content-type").replace(origCharset, charset) + ); + } else { + mimePart.headers._rawHeaders.set( + "content-type", + getHeaderValue(mimePart, "content-type") + "; charset=" + charset + ); + } + + this.cryptoChanged = true; + return 1; + } + + let ct = getContentType(mimePart); + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: Decryption skipped: " + ct + "\n" + ); + + return 0; + }, + + stripHTMLFromArmoredBlocks(text) { + var index = 0; + var begin = text.indexOf("-----BEGIN PGP"); + var end = text.indexOf("-----END PGP"); + + while (begin > -1 && end > -1) { + let sub = text.substring(begin, end); + + sub = sub.replace(/(<([^>]+)>)/gi, ""); + sub = sub.replace(/&[A-z]+;/gi, ""); + + text = text.substring(0, begin) + sub + text.substring(end); + + index = end + 10; + begin = text.indexOf("-----BEGIN PGP", index); + end = text.indexOf("-----END PGP", index); + } + + return text; + }, + + /****** + * + * We have the technology we can rebuild. + * + * Function to reassemble the message from the MIME Tree + * into a String. + * + ******/ + + mimeToString(mimePart, includeHeaders) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: mimeToString: part: '" + mimePart.partNum + "'\n" + ); + + let msg = ""; + let rawHdr = mimePart.headers._rawHeaders; + + if (includeHeaders && rawHdr.size > 0) { + for (let hdr of rawHdr.keys()) { + let formatted = formatMimeHeader(hdr, rawHdr.get(hdr)); + msg += formatted; + if (!formatted.endsWith("\r\n")) { + msg += "\r\n"; + } + } + + msg += "\r\n"; + } + + if (mimePart.body.length > 0) { + let encoding = getTransferEncoding(mimePart); + if (!encoding) { + encoding = "8bit"; + } + + if (encoding === "base64") { + msg += lazy.EnigmailData.encodeBase64(mimePart.body); + } else { + let charset = getCharset(mimePart, "content-type"); + if (charset) { + msg += lazy.EnigmailData.convertFromUnicode(mimePart.body, charset); + } else { + msg += mimePart.body; + } + } + } + + if (mimePart.subParts.length > 0) { + let boundary = lazy.EnigmailMime.getBoundary( + rawHdr.get("content-type").join("") + ); + + for (let i in mimePart.subParts) { + msg += `--${boundary}\r\n`; + msg += this.mimeToString(mimePart.subParts[i], true); + if (msg.search(/[\r\n]$/) < 0) { + msg += "\r\n"; + } + msg += "\r\n"; + } + + msg += `--${boundary}--\r\n`; + } + return msg; + }, + + fixExchangeMessage(mimePart) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: fixExchangeMessage()\n"); + + let msg = this.mimeToString(mimePart, true); + + try { + let fixedMsg = lazy.EnigmailFixExchangeMsg.getRepairedMessage(msg); + let replacement = lazy.EnigmailMime.getMimeTree(fixedMsg, true); + + for (let i in replacement) { + mimePart[i] = replacement[i]; + } + } catch (ex) {} + }, +}; + +/** + * Format a mime header + * + * e.g. content-type -> Content-Type + */ + +function formatHeader(headerLabel) { + return headerLabel.replace(/^.|(-.)/g, function (match) { + return match.toUpperCase(); + }); +} + +function formatMimeHeader(headerLabel, headerValue) { + if (Array.isArray(headerValue)) { + return headerValue + .map(v => formatHeader(headerLabel) + ": " + v) + .join("\r\n"); + } + return formatHeader(headerLabel) + ": " + headerValue + "\r\n"; +} + +function prettyPrintHeader(headerLabel, headerData) { + if (Array.isArray(headerData)) { + let h = []; + for (let i in headerData) { + h.push( + formatMimeHeader(headerLabel, lazy.GlodaUtils.deMime(headerData[i])) + ); + } + return h.join("\r\n"); + } + return formatMimeHeader( + headerLabel, + lazy.GlodaUtils.deMime(String(headerData)) + ); +} + +function getHeaderValue(mimeStruct, header) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: getHeaderValue: '" + header + "'\n" + ); + + try { + if (mimeStruct.headers.has(header)) { + let hdrVal = mimeStruct.headers.get(header); + if (typeof hdrVal == "string") { + return hdrVal; + } + return mimeStruct.headers[header].join(" "); + } + return ""; + } catch (ex) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: getHeaderValue: header not present\n" + ); + return ""; + } +} + +function getContentType(mime) { + try { + if (mime && "headers" in mime && mime.headers.has("content-type")) { + return mime.headers.get("content-type").type.toLowerCase(); + } + } catch (e) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getContentType: " + e + "\n"); + } + return null; +} + +// return the content of the boundary parameter +function getBoundary(mime) { + try { + if (mime && "headers" in mime && mime.headers.has("content-type")) { + return mime.headers.get("content-type").get("boundary"); + } + } catch (e) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getBoundary: " + e + "\n"); + } + return null; +} + +function getCharset(mime) { + try { + if (mime && "headers" in mime && mime.headers.has("content-type")) { + let c = mime.headers.get("content-type").get("charset"); + if (c) { + return c.toLowerCase(); + } + } + } catch (e) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getCharset: " + e + "\n"); + } + return null; +} + +function getTransferEncoding(mime) { + try { + if ( + mime && + "headers" in mime && + mime.headers._rawHeaders.has("content-transfer-encoding") + ) { + let c = mime.headers._rawHeaders.get("content-transfer-encoding")[0]; + if (c) { + return c.toLowerCase(); + } + } + } catch (e) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: getTransferEncoding: " + e + "\n" + ); + } + return "8Bit"; +} + +function isAttachment(mime) { + try { + if (mime && "headers" in mime) { + if (mime.fullContentType.search(/^multipart\//i) === 0) { + return false; + } + if (mime.fullContentType.search(/^text\//i) < 0) { + return true; + } + + if (mime.headers.has("content-disposition")) { + let c = mime.headers.get("content-disposition")[0]; + if (c) { + if (c.search(/^attachment/i) === 0) { + return true; + } + } + } + } + } catch (x) {} + return false; +} + +/** + * If the given MIME part is an attachment, return its filename. + * + * @param mime: a MIME part + * @return: the filename or null + */ +function getAttachmentName(mime) { + if ("headers" in mime && mime.headers.has("content-disposition")) { + let c = mime.headers.get("content-disposition")[0]; + if (/^attachment/i.test(c)) { + return lazy.EnigmailMime.getParameter(c, "filename"); + } + } + return null; +} + +function getPepSubject(mimeString) { + lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getPepSubject()\n"); + + let subject = null; + + let emitter = { + ct: "", + firstPlainText: false, + startPart(partNum, headers) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: getPepSubject.startPart: partNum=" + + partNum + + "\n" + ); + try { + this.ct = String(headers.getRawHeader("content-type")).toLowerCase(); + if (!subject && !this.firstPlainText) { + let s = headers.getRawHeader("subject"); + if (s) { + subject = String(s); + this.firstPlainText = true; + } + } + } catch (ex) { + this.ct = ""; + } + }, + + endPart(partNum) {}, + + deliverPartData(partNum, data) { + lazy.EnigmailLog.DEBUG( + "persistentCrypto.jsm: getPepSubject.deliverPartData: partNum=" + + partNum + + " ct=" + + this.ct + + "\n" + ); + if (!this.firstPlainText && this.ct.search(/^text\/plain/) === 0) { + // check data + this.firstPlainText = true; + + let o = lazy.EnigmailMime.extractSubjectFromBody(data); + if (o) { + subject = o.subject; + } + } + }, + }; + + let opt = { + strformat: "unicode", + bodyformat: "decode", + }; + + try { + let p = new lazy.jsmime.MimeParser(emitter, opt); + p.deliverData(mimeString); + } catch (ex) {} + + return subject; +} |