diff options
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm')
-rw-r--r-- | comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm | 760 |
1 files changed, 760 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm b/comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm new file mode 100644 index 0000000000..dd4018d704 --- /dev/null +++ b/comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm @@ -0,0 +1,760 @@ +/* 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"; + +/** + * Module for creating PGP/MIME signed and/or encrypted messages + * implemented as XPCOM component + */ + +const EXPORTED_SYMBOLS = ["EnigmailMimeEncrypt"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm", + EnigmailData: "chrome://openpgp/content/modules/data.jsm", + EnigmailEncryption: "chrome://openpgp/content/modules/encryption.jsm", + EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm", + EnigmailLog: "chrome://openpgp/content/modules/log.jsm", + EnigmailMime: "chrome://openpgp/content/modules/mime.jsm", + jsmime: "resource:///modules/jsmime.jsm", +}); + +// our own contract IDs +const PGPMIME_ENCRYPT_CID = Components.ID( + "{96fe88f9-d2cd-466f-93e0-3a351df4c6d2}" +); +const PGPMIME_ENCRYPT_CONTRACTID = "@enigmail.net/compose/mimeencrypt;1"; + +const maxBufferLen = 102400; +const MIME_SIGNED = 1; // only one MIME layer +const MIME_ENCRYPTED = 2; // only one MIME layer, combined enc/sig data +const MIME_OUTER_ENC_INNER_SIG = 3; // use two MIME layers + +var gDebugLogLevel = 1; + +function PgpMimeEncrypt(sMimeSecurityInfo) { + this.wrappedJSObject = this; + + this.signMessage = false; + this.requireEncryptMessage = false; + + // "securityInfo" variables + this.sendFlags = 0; + this.UIFlags = 0; + this.senderEmailAddr = ""; + this.recipients = ""; + this.bccRecipients = ""; + this.originalSubject = null; + this.autocryptGossipHeaders = ""; + + try { + if (sMimeSecurityInfo) { + this.signMessage = sMimeSecurityInfo.signMessage; + this.requireEncryptMessage = sMimeSecurityInfo.requireEncryptMessage; + } + } catch (ex) {} +} + +PgpMimeEncrypt.prototype = { + classDescription: "Enigmail JS Encryption Handler", + classID: PGPMIME_ENCRYPT_CID, + get contractID() { + return PGPMIME_ENCRYPT_CONTRACTID; + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgComposeSecure", + "nsIStreamListener", + ]), + + signMessage: false, + requireEncryptMessage: false, + + // private variables + + inStream: Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ), + msgCompFields: null, + outStringStream: null, + + // 0: processing headers + // 1: processing body + // 2: skipping header + inputMode: 0, + headerData: "", + encapsulate: null, + encHeader: null, + outerBoundary: null, + innerBoundary: null, + win: null, + //statusStr: "", + cryptoOutputLength: 0, + cryptoOutput: "", + hashAlgorithm: "SHA256", // TODO: coordinate with RNP.jsm + cryptoInputBuffer: "", + outgoingMessageBuffer: "", + mimeStructure: 0, + exitCode: -1, + inspector: null, + + // nsIStreamListener interface + onStartRequest(request) { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: onStartRequest\n"); + this.encHeader = null; + }, + + onDataAvailable(req, stream, offset, count) { + LOCAL_DEBUG("mimeEncrypt.js: onDataAvailable\n"); + this.inStream.init(stream); + //var data = this.inStream.read(count); + //LOCAL_DEBUG("mimeEncrypt.js: >"+data+"<\n"); + }, + + onStopRequest(request, status) { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: onStopRequest\n"); + }, + + // nsIMsgComposeSecure interface + requiresCryptoEncapsulation(msgIdentity, msgCompFields) { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: requiresCryptoEncapsulation\n"); + return ( + (this.sendFlags & + (lazy.EnigmailConstants.SEND_SIGNED | + lazy.EnigmailConstants.SEND_ENCRYPTED | + lazy.EnigmailConstants.SEND_VERBATIM)) !== + 0 + ); + }, + + beginCryptoEncapsulation( + outStream, + recipientList, + msgCompFields, + msgIdentity, + sendReport, + isDraft + ) { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: beginCryptoEncapsulation\n"); + + if (!outStream) { + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + try { + this.outStream = outStream; + this.isDraft = isDraft; + + this.msgCompFields = msgCompFields; + this.outStringStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + var windowManager = Services.wm; + this.win = windowManager.getMostRecentWindow(null); + + if (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) { + this.recipientList = recipientList; + this.msgIdentity = msgIdentity; + this.msgCompFields = msgCompFields; + this.inputMode = 2; + return null; + } + + if (this.sendFlags & lazy.EnigmailConstants.SEND_PGP_MIME) { + if (this.sendFlags & lazy.EnigmailConstants.SEND_ENCRYPTED) { + // applies to encrypted and signed & encrypted + if (this.sendFlags & lazy.EnigmailConstants.SEND_TWO_MIME_LAYERS) { + this.mimeStructure = MIME_OUTER_ENC_INNER_SIG; + this.innerBoundary = lazy.EnigmailMime.createBoundary(); + } else { + this.mimeStructure = MIME_ENCRYPTED; + } + } else if (this.sendFlags & lazy.EnigmailConstants.SEND_SIGNED) { + this.mimeStructure = MIME_SIGNED; + } + } else { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + this.outerBoundary = lazy.EnigmailMime.createBoundary(); + this.startCryptoHeaders(); + } catch (ex) { + console.debug(ex); + lazy.EnigmailLog.writeException("mimeEncrypt.js", ex); + throw ex; + } + + return null; + }, + + startCryptoHeaders() { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: startCryptoHeaders\n"); + + switch (this.mimeStructure) { + case MIME_SIGNED: + this.signedHeaders1(false); + break; + case MIME_ENCRYPTED: + case MIME_OUTER_ENC_INNER_SIG: + this.encryptedHeaders(); + break; + } + + this.writeSecureHeaders(); + }, + + writeSecureHeaders() { + this.encHeader = lazy.EnigmailMime.createBoundary(); + + let allHdr = ""; + + let addrParser = lazy.jsmime.headerparser.parseAddressingHeader; + let newsParser = function (s) { + return lazy.jsmime.headerparser.parseStructuredHeader("Newsgroups", s); + }; + let noParser = function (s) { + return s; + }; + + let h = { + from: { + field: "From", + parser: addrParser, + }, + replyTo: { + field: "Reply-To", + parser: addrParser, + }, + to: { + field: "To", + parser: addrParser, + }, + cc: { + field: "Cc", + parser: addrParser, + }, + newsgroups: { + field: "Newsgroups", + parser: newsParser, + }, + followupTo: { + field: "Followup-To", + parser: addrParser, + }, + messageId: { + field: "Message-Id", + parser: noParser, + }, + subject: { + field: "Subject", + parser: noParser, + }, + }; + + let alreadyAddedSubject = false; + + if ( + (this.mimeStructure == MIME_ENCRYPTED || + this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) && + this.originalSubject && + this.originalSubject.length > 0 + ) { + alreadyAddedSubject = true; + allHdr += lazy.jsmime.headeremitter.emitStructuredHeader( + "subject", + this.originalSubject, + {} + ); + } + + for (let i in h) { + if (h[i].field == "Subject" && alreadyAddedSubject) { + continue; + } + if (this.msgCompFields[i] && this.msgCompFields[i].length > 0) { + allHdr += lazy.jsmime.headeremitter.emitStructuredHeader( + h[i].field, + h[i].parser(this.msgCompFields[i]), + {} + ); + } + } + + // special handling for references and in-reply-to + + if (this.originalReferences && this.originalReferences.length > 0) { + allHdr += lazy.jsmime.headeremitter.emitStructuredHeader( + "references", + this.originalReferences, + {} + ); + + let bracket = this.originalReferences.lastIndexOf("<"); + if (bracket >= 0) { + allHdr += lazy.jsmime.headeremitter.emitStructuredHeader( + "in-reply-to", + this.originalReferences.substr(bracket), + {} + ); + } + } + + let w = `Content-Type: multipart/mixed; boundary="${this.encHeader}"`; + + if (allHdr.length > 0) { + w += `;\r\n protected-headers="v1"\r\n${allHdr}`; + } else { + w += "\r\n"; + } + + if (this.autocryptGossipHeaders) { + w += this.autocryptGossipHeaders; + } + + w += `\r\n--${this.encHeader}\r\n`; + this.appendToCryptoInput(w); + + if (this.mimeStructure == MIME_SIGNED) { + this.appendToMessage(w); + } + }, + + encryptedHeaders(isEightBit = false) { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: encryptedHeaders\n"); + let subj = ""; + + if (this.sendFlags & lazy.EnigmailConstants.ENCRYPT_SUBJECT) { + subj = lazy.jsmime.headeremitter.emitStructuredHeader( + "subject", + lazy.EnigmailFuncs.getProtectedSubjectText(), + {} + ); + } + this.appendToMessage( + subj + + "Content-Type: multipart/encrypted;\r\n" + + ' protocol="application/pgp-encrypted";\r\n' + + ' boundary="' + + this.outerBoundary + + '"\r\n' + + "\r\n" + + "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n" + + "--" + + this.outerBoundary + + "\r\n" + + "Content-Type: application/pgp-encrypted\r\n" + + "Content-Description: PGP/MIME version identification\r\n" + + "\r\n" + + "Version: 1\r\n" + + "\r\n" + + "--" + + this.outerBoundary + + "\r\n" + + 'Content-Type: application/octet-stream; name="encrypted.asc"\r\n' + + "Content-Description: OpenPGP encrypted message\r\n" + + 'Content-Disposition: inline; filename="encrypted.asc"\r\n' + + "\r\n" + ); + }, + + signedHeaders1(isEightBit = false) { + LOCAL_DEBUG("mimeEncrypt.js: signedHeaders1\n"); + let boundary; + if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) { + boundary = this.innerBoundary; + } else { + boundary = this.outerBoundary; + } + let sigHeader = + "Content-Type: multipart/signed; micalg=pgp-" + + this.hashAlgorithm.toLowerCase() + + ";\r\n" + + ' protocol="application/pgp-signature";\r\n' + + ' boundary="' + + boundary + + '"\r\n' + + (isEightBit ? "Content-Transfer-Encoding: 8bit\r\n\r\n" : "\r\n") + + "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n" + + "--" + + boundary + + "\r\n"; + if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) { + this.appendToCryptoInput(sigHeader); + } else { + this.appendToMessage(sigHeader); + } + }, + + signedHeaders2() { + LOCAL_DEBUG("mimeEncrypt.js: signedHeaders2\n"); + let boundary; + if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) { + boundary = this.innerBoundary; + } else { + boundary = this.outerBoundary; + } + let sigHeader = + "\r\n--" + + boundary + + "\r\n" + + 'Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"\r\n' + + "Content-Description: OpenPGP digital signature\r\n" + + 'Content-Disposition: attachment; filename="OpenPGP_signature.asc"\r\n\r\n'; + if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) { + this.appendToCryptoInput(sigHeader); + } else { + this.appendToMessage(sigHeader); + } + }, + + finishCryptoHeaders() { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: finishCryptoHeaders\n"); + + this.appendToMessage("\r\n--" + this.outerBoundary + "--\r\n"); + }, + + finishCryptoEncapsulation(abort, sendReport) { + lazy.EnigmailLog.DEBUG("mimeEncrypt.js: finishCryptoEncapsulation\n"); + + if ((this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== 0) { + this.flushOutput(); + return; + } + + if (this.encapsulate) { + this.appendToCryptoInput("--" + this.encapsulate + "--\r\n"); + } + + if (this.encHeader) { + this.appendToCryptoInput("\r\n--" + this.encHeader + "--\r\n"); + if (this.mimeStructure == MIME_SIGNED) { + this.appendToMessage("\r\n--" + this.encHeader + "--\r\n"); + } + } + + let statusFlagsObj = {}; + let errorMsgObj = {}; + this.exitCode = 0; + + if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) { + // prepare the inner crypto layer (the signature) + let sendFlagsWithoutEncrypt = + this.sendFlags & ~lazy.EnigmailConstants.SEND_ENCRYPTED; + + this.exitCode = lazy.EnigmailEncryption.encryptMessageStart( + this.win, + this.UIFlags, + this.senderEmailAddr, + this.recipients, + this.bccRecipients, + this.hashAlgorithm, + sendFlagsWithoutEncrypt, + this, + statusFlagsObj, + errorMsgObj + ); + if (!this.exitCode) { + // success + let innerSignedMessage = this.cryptoInputBuffer; + this.cryptoInputBuffer = ""; + + this.signedHeaders1(false); + this.appendToCryptoInput(innerSignedMessage); + this.signedHeaders2(); + this.cryptoOutput = this.cryptoOutput + .replace(/\r/g, "") + .replace(/\n/g, "\r\n"); // force CRLF + this.appendToCryptoInput(this.cryptoOutput); + this.appendToCryptoInput("\r\n--" + this.innerBoundary + "--\r\n"); + this.cryptoOutput = ""; + } + } + + if (!this.exitCode) { + // no failure yet + let encryptionFlags = this.sendFlags; + if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) { + // remove signature flag, because we already signed + encryptionFlags = encryptionFlags & ~lazy.EnigmailConstants.SEND_SIGNED; + } + this.exitCode = lazy.EnigmailEncryption.encryptMessageStart( + this.win, + this.UIFlags, + this.senderEmailAddr, + this.recipients, + this.bccRecipients, + this.hashAlgorithm, + encryptionFlags, + this, + statusFlagsObj, + errorMsgObj + ); + } + + try { + LOCAL_DEBUG( + "mimeEncrypt.js: finishCryptoEncapsulation: exitCode = " + + this.exitCode + + "\n" + ); + if (this.exitCode !== 0) { + throw new Error( + "failure in finishCryptoEncapsulation, exitCode: " + this.exitCode + ); + } + + if (this.mimeStructure == MIME_SIGNED) { + this.signedHeaders2(); + } + + this.cryptoOutput = this.cryptoOutput + .replace(/\r/g, "") + .replace(/\n/g, "\r\n"); // force CRLF + + this.appendToMessage(this.cryptoOutput); + this.finishCryptoHeaders(); + this.flushOutput(); + } catch (ex) { + console.debug(ex); + lazy.EnigmailLog.writeException("mimeEncrypt.js", ex); + throw ex; + } + }, + + mimeCryptoWriteBlock(buffer, length) { + if (gDebugLogLevel > 4) { + LOCAL_DEBUG("mimeEncrypt.js: mimeCryptoWriteBlock: " + length + "\n"); + } + + try { + let line = buffer.substr(0, length); + if (this.inputMode === 0) { + if ((this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== 0) { + line = lazy.EnigmailData.decodeQuotedPrintable( + line.replace("=\r\n", "") + ); + } + + if ( + (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) === 0 || + line.match( + /^(From|To|Subject|Message-ID|Date|User-Agent|MIME-Version):/i + ) === null + ) { + this.headerData += line; + } + + if (line.replace(/[\r\n]/g, "").length === 0) { + this.inputMode = 1; + + if ( + this.mimeStructure == MIME_ENCRYPTED || + this.mimeStructure == MIME_OUTER_ENC_INNER_SIG + ) { + if (!this.encHeader) { + let ct = this.getHeader("content-type", false); + if ( + ct.search(/text\/plain/i) === 0 || + ct.search(/text\/html/i) === 0 + ) { + this.encapsulate = lazy.EnigmailMime.createBoundary(); + this.appendToCryptoInput( + 'Content-Type: multipart/mixed; boundary="' + + this.encapsulate + + '"\r\n\r\n' + ); + this.appendToCryptoInput("--" + this.encapsulate + "\r\n"); + } + } + } else if (this.mimeStructure == MIME_SIGNED) { + let ct = this.getHeader("content-type", true); + let hdr = lazy.EnigmailFuncs.getHeaderData(ct); + hdr.boundary = hdr.boundary || ""; + hdr.boundary = hdr.boundary.replace(/^(['"])(.*)(\1)$/, "$2"); + } + + this.appendToCryptoInput(this.headerData); + if ( + this.mimeStructure == MIME_SIGNED || + (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== 0 + ) { + this.appendToMessage(this.headerData); + } + } + } else if (this.inputMode == 1) { + if (this.mimeStructure == MIME_SIGNED) { + // special treatments for various special cases with PGP/MIME signed messages + if (line.substr(0, 5) == "From ") { + LOCAL_DEBUG("mimeEncrypt.js: added >From\n"); + this.appendToCryptoInput(">"); + } + } + + this.appendToCryptoInput(line); + if (this.mimeStructure == MIME_SIGNED) { + this.appendToMessage(line); + } else if ( + (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== + 0 + ) { + this.appendToMessage( + lazy.EnigmailData.decodeQuotedPrintable(line.replace("=\r\n", "")) + ); + } + } else if (this.inputMode == 2) { + if (line.replace(/[\r\n]/g, "").length === 0) { + this.inputMode = 0; + } + } + } catch (ex) { + console.debug(ex); + lazy.EnigmailLog.writeException("mimeEncrypt.js", ex); + throw ex; + } + + return null; + }, + + appendToMessage(str) { + if (gDebugLogLevel > 4) { + LOCAL_DEBUG("mimeEncrypt.js: appendToMessage: " + str.length + "\n"); + } + + this.outgoingMessageBuffer += str; + + if (this.outgoingMessageBuffer.length > maxBufferLen) { + this.flushOutput(); + } + }, + + flushOutput() { + LOCAL_DEBUG( + "mimeEncrypt.js: flushOutput: " + this.outgoingMessageBuffer.length + "\n" + ); + + this.outStringStream.setData( + this.outgoingMessageBuffer, + this.outgoingMessageBuffer.length + ); + var writeCount = this.outStream.writeFrom( + this.outStringStream, + this.outgoingMessageBuffer.length + ); + if (writeCount < this.outgoingMessageBuffer.length) { + LOCAL_DEBUG( + "mimeEncrypt.js: flushOutput: wrote " + + writeCount + + " instead of " + + this.outgoingMessageBuffer.length + + " bytes\n" + ); + } + this.outgoingMessageBuffer = ""; + }, + + appendToCryptoInput(str) { + if (gDebugLogLevel > 4) { + LOCAL_DEBUG("mimeEncrypt.js: appendToCryptoInput: " + str.length + "\n"); + } + + this.cryptoInputBuffer += str; + }, + + getHeader(hdrStr, fullHeader) { + var res = ""; + var hdrLines = this.headerData.split(/[\r\n]+/); + for (let i = 0; i < hdrLines.length; i++) { + if (hdrLines[i].length > 0) { + if (fullHeader && res !== "") { + if (hdrLines[i].search(/^\s+/) === 0) { + res += hdrLines[i].replace(/\s*[\r\n]*$/, ""); + } else { + return res; + } + } else { + let j = hdrLines[i].indexOf(":"); + if (j > 0) { + let h = hdrLines[i].substr(0, j).replace(/\s*$/, ""); + if (h.toLowerCase() == hdrStr.toLowerCase()) { + res = hdrLines[i].substr(j + 1).replace(/^\s*/, ""); + if (!fullHeader) { + return res; + } + } + } + } + } + } + + return res; + }, + + getInputForCrypto() { + return this.cryptoInputBuffer; + }, + + addCryptoOutput(s) { + LOCAL_DEBUG("mimeEncrypt.js: addCryptoOutput:" + s.length + "\n"); + this.cryptoOutput += s; + this.cryptoOutputLength += s.length; + }, + + getCryptoOutputLength() { + return this.cryptoOutputLength; + }, + + // API for decryptMessage Listener + stdin(pipe) { + throw new Error("unexpected"); + }, + + stderr(s) { + throw new Error("unexpected"); + //this.statusStr += s; + }, +}; + +//////////////////////////////////////////////////////////////////// +// General-purpose functions, not exported + +function LOCAL_DEBUG(str) { + if (gDebugLogLevel) { + lazy.EnigmailLog.DEBUG(str); + } +} + +function initModule() { + lazy.EnigmailLog.DEBUG("mimeEncrypt.jsm: initModule()\n"); + let nspr_log_modules = Services.env.get("NSPR_LOG_MODULES"); + let matches = nspr_log_modules.match(/mimeEncrypt:(\d+)/); + + if (matches && matches.length > 1) { + gDebugLogLevel = matches[1]; + LOCAL_DEBUG("mimeEncrypt.js: enabled debug logging\n"); + } +} + +var EnigmailMimeEncrypt = { + Handler: PgpMimeEncrypt, + + startup(reason) { + initModule(); + }, + shutdown(reason) {}, + + createMimeEncrypt(sMimeSecurityInfo) { + return new PgpMimeEncrypt(sMimeSecurityInfo); + }, + + isEnigmailCompField(obj) { + return obj instanceof PgpMimeEncrypt; + }, +}; |