summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm')
-rw-r--r--comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm716
1 files changed, 716 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm b/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm
new file mode 100644
index 0000000000..6ec7a615c2
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm
@@ -0,0 +1,716 @@
+/* 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 EXPORTED_SYMBOLS = ["EnigmailVerify"];
+
+/**
+ * Module for handling PGP/MIME signed messages implemented as JS module.
+ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+});
+
+const PGPMIME_PROTO = "application/pgp-signature";
+
+var gDebugLog = false;
+
+// MimeVerify Constructor
+function MimeVerify(protocol) {
+ if (!protocol) {
+ protocol = PGPMIME_PROTO;
+ }
+
+ this.protocol = protocol;
+ this.verifyEmbedded = false;
+ this.partiallySigned = false;
+ this.exitCode = null;
+ this.inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+}
+
+var EnigmailVerify = {
+ _initialized: false,
+ lastWindow: null,
+ lastMsgUri: null,
+ manualMsgUri: null,
+
+ currentCtHandler: EnigmailConstants.MIME_HANDLER_UNDEF,
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ let nspr_log_modules = Services.env.get("NSPR_LOG_MODULES");
+ let matches = nspr_log_modules.match(/mimeVerify:(\d+)/);
+
+ if (matches && matches.length > 1) {
+ if (matches[1] > 2) {
+ gDebugLog = true;
+ }
+ }
+ },
+
+ setWindow(window, msgUriSpec) {
+ LOCAL_DEBUG("mimeVerify.jsm: setWindow: " + msgUriSpec + "\n");
+
+ this.lastWindow = window;
+ this.lastMsgUri = msgUriSpec;
+ },
+
+ newVerifier(protocol) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: newVerifier: " + (protocol || "null") + "\n"
+ );
+
+ let v = new MimeVerify(protocol);
+ return v;
+ },
+
+ setManualUri(msgUriSpec) {
+ LOCAL_DEBUG("mimeVerify.jsm: setManualUri: " + msgUriSpec + "\n");
+ this.manualMsgUri = msgUriSpec;
+ },
+
+ getManualUri() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: getManualUri\n");
+ return this.manualMsgUri;
+ },
+
+ pgpMimeFactory: {
+ classID: Components.ID("{4f4400a8-9bcc-4b9d-9d53-d2437b377e29}"),
+ createInstance(iid) {
+ return Cc[
+ "@mozilla.org/mimecth;1?type=multipart/encrypted"
+ ].createInstance(iid);
+ },
+ },
+
+ /**
+ * Sets the PGPMime content type handler as the registered handler.
+ */
+ registerPGPMimeHandler() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: registerPGPMimeHandler\n");
+
+ if (this.currentCtHandler == EnigmailConstants.MIME_HANDLER_PGPMIME) {
+ return;
+ }
+
+ let reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ reg.registerFactory(
+ this.pgpMimeFactory.classID,
+ "PGP/MIME verification",
+ "@mozilla.org/mimecth;1?type=multipart/signed",
+ this.pgpMimeFactory
+ );
+
+ this.currentCtHandler = EnigmailConstants.MIME_HANDLER_PGPMIME;
+ },
+
+ /**
+ * Clears the PGPMime content type handler registration. If no factory is
+ * registered, S/MIME works.
+ */
+ unregisterPGPMimeHandler() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: unregisterPGPMimeHandler\n");
+
+ let reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (this.currentCtHandler == EnigmailConstants.MIME_HANDLER_PGPMIME) {
+ reg.unregisterFactory(this.pgpMimeFactory.classID, this.pgpMimeFactory);
+ }
+
+ this.currentCtHandler = EnigmailConstants.MIME_HANDLER_SMIME;
+ },
+};
+
+// MimeVerify implementation
+// verify the signature of PGP/MIME signed messages
+MimeVerify.prototype = {
+ dataCount: 0,
+ foundMsg: false,
+ startMsgStr: "",
+ window: null,
+ msgUriSpec: null,
+ statusDisplayed: false,
+ inStream: null,
+ sigFile: null,
+ sigData: "",
+ mimePartNumber: "",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ parseContentType() {
+ let contentTypeLine = this.mimeSvc.contentType;
+
+ // Eat up CRLF's.
+ contentTypeLine = contentTypeLine.replace(/[\r\n]/g, "");
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: parseContentType: " + contentTypeLine + "\n"
+ );
+
+ let protoRx = RegExp(
+ "protocol\\s*=\\s*[\\'\\\"]" + this.protocol + "[\\\"\\']",
+ "i"
+ );
+
+ if (
+ contentTypeLine.search(/multipart\/signed/i) >= 0 &&
+ contentTypeLine.search(protoRx) > 0
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: parseContentType: found MIME signed message\n"
+ );
+ this.foundMsg = true;
+ let hdr = lazy.EnigmailFuncs.getHeaderData(contentTypeLine);
+ hdr.boundary = hdr.boundary || "";
+ hdr.micalg = hdr.micalg || "";
+ this.boundary = hdr.boundary.replace(/^(['"])(.*)(\1)$/, "$2");
+ }
+ },
+
+ onStartRequest(request, uri) {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: onStartRequest\n"); // always log this one
+
+ this.mimeSvc = request.QueryInterface(Ci.nsIPgpMimeProxy);
+ this.msgUriSpec = EnigmailVerify.lastMsgUri;
+
+ if ("mimePart" in this.mimeSvc) {
+ this.mimePartNumber = this.mimeSvc.mimePart;
+ } else {
+ this.mimePartNumber = "";
+ }
+
+ if ("messageURI" in this.mimeSvc) {
+ this.uri = this.mimeSvc.messageURI;
+ } else if (uri) {
+ this.uri = uri.QueryInterface(Ci.nsIURI);
+ }
+
+ this.dataCount = 0;
+ this.foundMsg = false;
+ this.backgroundJob = false;
+ this.startMsgStr = "";
+ this.boundary = "";
+ this.proc = null;
+ this.closePipe = false;
+ this.pipe = null;
+ this.readMode = 0;
+ this.keepData = "";
+ this.last80Chars = "";
+ this.signedData = "";
+ this.statusStr = "";
+ this.returnStatus = null;
+ this.statusDisplayed = false;
+ this.protectedHeaders = null;
+ this.parseContentType();
+ },
+
+ onDataAvailable(req, stream, offset, count) {
+ LOCAL_DEBUG("mimeVerify.jsm: onDataAvailable: " + count + "\n");
+ if (count > 0) {
+ this.inStream.init(stream);
+ var data = this.inStream.read(count);
+ this.onTextData(data);
+ }
+ },
+
+ onTextData(data) {
+ LOCAL_DEBUG("mimeVerify.jsm: onTextData\n");
+
+ this.dataCount += data.length;
+
+ this.keepData += data;
+ if (this.readMode === 0) {
+ // header data
+ let i = this.findNextMimePart();
+ if (i >= 0) {
+ i += 2 + this.boundary.length;
+ if (this.keepData[i] == "\n") {
+ ++i;
+ } else if (this.keepData[i] == "\r") {
+ ++i;
+ if (this.keepData[i] == "\n") {
+ ++i;
+ }
+ }
+
+ this.keepData = this.keepData.substr(i);
+ data = this.keepData;
+ this.readMode = 1;
+ } else {
+ this.keepData = data.substr(-this.boundary.length - 3);
+ }
+ }
+
+ if (this.readMode === 1) {
+ // "real data"
+ if (data.includes("-")) {
+ // only check current line for speed reasons
+ let i = this.findNextMimePart();
+ if (i >= 0) {
+ // end of "read data found"
+ if (this.keepData[i - 2] == "\r" && this.keepData[i - 1] == "\n") {
+ --i;
+ }
+
+ this.signedData = this.keepData.substr(0, i - 1);
+ this.keepData = this.keepData.substr(i);
+ this.readMode = 2;
+ }
+ } else {
+ return;
+ }
+ }
+
+ if (this.readMode === 2) {
+ let i = this.keepData.indexOf("--" + this.boundary + "--");
+ if (i >= 0) {
+ // ensure that we keep everything until we got the "end" boundary
+ if (this.keepData[i - 2] == "\r" && this.keepData[i - 1] == "\n") {
+ --i;
+ }
+ this.keepData = this.keepData.substr(0, i - 1);
+ this.readMode = 3;
+ }
+ }
+
+ if (this.readMode === 3) {
+ // signature data
+ if (this.protocol === PGPMIME_PROTO) {
+ let xferEnc = this.getContentTransferEncoding();
+ if (xferEnc.search(/base64/i) >= 0) {
+ let bound = this.getBodyPart();
+ this.keepData =
+ lazy.EnigmailData.decodeBase64(
+ this.keepData.substring(bound.start, bound.end)
+ ) + "\n";
+ } else if (xferEnc.search(/quoted-printable/i) >= 0) {
+ let bound = this.getBodyPart();
+ let qp = this.keepData.substring(bound.start, bound.end);
+ this.keepData = lazy.EnigmailData.decodeQuotedPrintable(qp) + "\n";
+ }
+
+ // extract signature data
+ let s = Math.max(this.keepData.search(/^-----BEGIN PGP /m), 0);
+ let e = Math.max(
+ this.keepData.search(/^-----END PGP /m),
+ this.keepData.length - 30
+ );
+ this.sigData = this.keepData.substring(s, e + 30);
+ } else {
+ this.sigData = "";
+ }
+
+ this.keepData = "";
+ this.readMode = 4; // ignore any further data
+ }
+ },
+
+ getBodyPart() {
+ let start = this.keepData.search(/(\n\n|\r\n\r\n)/);
+ if (start < 0) {
+ start = 0;
+ }
+ let end = this.keepData.indexOf("--" + this.boundary + "--") - 1;
+ if (end < 0) {
+ end = this.keepData.length;
+ }
+
+ return {
+ start,
+ end,
+ };
+ },
+
+ // determine content-transfer encoding of mime part, assuming that whole
+ // message is in this.keepData
+ getContentTransferEncoding() {
+ let enc = "7bit";
+ let m = this.keepData.match(/^(content-transfer-encoding:)(.*)$/im);
+ if (m && m.length > 2) {
+ enc = m[2].trim().toLowerCase();
+ }
+
+ return enc;
+ },
+
+ findNextMimePart() {
+ let startOk = false;
+ let endOk = false;
+
+ let i = this.keepData.indexOf("--" + this.boundary);
+ if (i === 0) {
+ startOk = true;
+ }
+ if (i > 0) {
+ if (this.keepData[i - 1] == "\r" || this.keepData[i - 1] == "\n") {
+ startOk = true;
+ }
+ }
+
+ if (!startOk) {
+ return -1;
+ }
+
+ if (i + this.boundary.length + 2 < this.keepData.length) {
+ if (
+ this.keepData[i + this.boundary.length + 2] == "\r" ||
+ this.keepData[i + this.boundary.length + 2] == "\n" ||
+ this.keepData.substr(i + this.boundary.length + 2, 2) == "--"
+ ) {
+ endOk = true;
+ }
+ }
+ // else
+ // endOk = true;
+
+ if (i >= 0 && startOk && endOk) {
+ return i;
+ }
+ return -1;
+ },
+
+ isAllowedSigPart(queryMimePartNumber, loadedUriSpec) {
+ // allowed are:
+ // - the top part 1
+ // - the child 1.1 if 1 is an encryption layer
+ // - a part that is the one we are loading
+ // - a part that is the first child of the one we are loading,
+ // and the child we are loading is an encryption layer
+
+ if (queryMimePartNumber.length === 0) {
+ return false;
+ }
+
+ if (queryMimePartNumber === "1") {
+ return true;
+ }
+
+ if (queryMimePartNumber == "1.1" || queryMimePartNumber == "1.1.1") {
+ if (!this.uri) {
+ // We aren't loading in message displaying, but some other
+ // context, could be e.g. forwarding.
+ return false;
+ }
+
+ // If we are processing "1.1", it means we're the child of the
+ // top mime part. Don't process the signature unless the top
+ // level mime part is an encryption layer.
+ // If we are processing "1.1.1", then potentially the top level
+ // mime part was a signature and has been ignored, and "1.1"
+ // might be an encrypted part that was allowed.
+
+ let currMsg = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+ let parentToCheck = queryMimePartNumber == "1.1.1" ? "1.1" : "1";
+ if (
+ lazy.EnigmailSingletons.isLastDecryptedMessagePart(
+ currMsg.folder,
+ currMsg.msgNum,
+ parentToCheck
+ )
+ ) {
+ return true;
+ }
+ }
+
+ if (!loadedUriSpec) {
+ return false;
+ }
+
+ // is the message a subpart of a complete attachment?
+ let msgPart = lazy.EnigmailMime.getMimePartNumber(loadedUriSpec);
+
+ if (msgPart.length > 0) {
+ if (queryMimePartNumber === msgPart + ".1") {
+ return true;
+ }
+
+ let currMsg = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+ if (
+ queryMimePartNumber === msgPart + ".1.1" &&
+ lazy.EnigmailSingletons.isLastDecryptedMessagePart(
+ currMsg.folder,
+ currMsg.msgNum,
+ msgPart + ".1"
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ onStopRequest() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: onStopRequest\n");
+
+ this.window = EnigmailVerify.lastWindow;
+ this.msgUriSpec = EnigmailVerify.lastMsgUri;
+
+ this.backgroundJob = false;
+
+ // don't try to verify if no message found
+ // if (this.verifyEmbedded && (!this.foundMsg)) return; // TODO - check
+
+ let href = Services.wm.getMostRecentWindow(null)?.document?.location.href;
+
+ if (
+ href == "about:blank" ||
+ href == "chrome://messenger/content/viewSource.xhtml"
+ ) {
+ return;
+ }
+
+ if (this.readMode < 4) {
+ // we got incomplete data; simply return what we got
+ this.returnData(
+ this.signedData.length > 0 ? this.signedData : this.keepData
+ );
+
+ return;
+ }
+
+ this.protectedHeaders = lazy.EnigmailMime.extractProtectedHeaders(
+ this.signedData
+ );
+
+ if (
+ this.protectedHeaders &&
+ this.protectedHeaders.startPos >= 0 &&
+ this.protectedHeaders.endPos > this.protectedHeaders.startPos
+ ) {
+ let r =
+ this.signedData.substr(0, this.protectedHeaders.startPos) +
+ this.signedData.substr(this.protectedHeaders.endPos);
+ this.returnData(r);
+ } else {
+ this.returnData(this.signedData);
+ }
+
+ if (!this.isAllowedSigPart(this.mimePartNumber, this.msgUriSpec)) {
+ return;
+ }
+
+ if (this.uri) {
+ // return if not decrypting currently displayed message (except if
+ // printing, replying, etc)
+
+ this.backgroundJob =
+ this.uri.spec.search(/[&?]header=(print|quotebody)/) >= 0;
+
+ try {
+ if (!Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ // "decrypt manually" mode
+ let manUrl = {};
+
+ if (EnigmailVerify.getManualUri()) {
+ manUrl = lazy.EnigmailFuncs.getUrlFromUriSpec(
+ EnigmailVerify.getManualUri()
+ );
+ }
+
+ // print a message if not message explicitly decrypted
+ let currUrlSpec = this.uri.spec.replace(
+ /(\?.*)(number=[0-9]*)(&.*)?$/,
+ "?$2"
+ );
+ let manUrlSpec = manUrl.spec.replace(
+ /(\?.*)(number=[0-9]*)(&.*)?$/,
+ "?$2"
+ );
+
+ if (!this.backgroundJob && currUrlSpec != manUrlSpec) {
+ return; // this.handleManualDecrypt();
+ }
+ }
+
+ if (
+ this.uri.spec.search(/[&?]header=[a-zA-Z0-9]*$/) < 0 &&
+ this.uri.spec.search(/[&?]part=[.0-9]+/) < 0 &&
+ this.uri.spec.search(/[&?]examineEncryptedParts=true/) < 0
+ ) {
+ if (this.uri.spec.search(/[&?]header=filter&.*$/) > 0) {
+ return;
+ }
+
+ let url = this.msgUriSpec
+ ? lazy.EnigmailFuncs.getUrlFromUriSpec(this.msgUriSpec)
+ : null;
+
+ if (url) {
+ let otherId = lazy.EnigmailURIs.msgIdentificationFromUrl(url);
+ let thisId = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+
+ if (
+ url.host !== this.uri.host ||
+ otherId.folder !== thisId.folder ||
+ otherId.msgNum !== thisId.msgNum
+ ) {
+ return;
+ }
+ }
+ }
+ } catch (ex) {
+ lazy.EnigmailLog.writeException("mimeVerify.jsm", ex);
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: error while processing " + this.msgUriSpec + "\n"
+ );
+ }
+ }
+
+ if (this.protocol === PGPMIME_PROTO) {
+ let win = this.window;
+
+ if (!lazy.EnigmailDecryption.isReady(win)) {
+ return;
+ }
+
+ let options = {
+ fromAddr: lazy.EnigmailDecryption.getFromAddr(win),
+ mimeSignatureData: this.sigData,
+ msgDate: lazy.EnigmailDecryption.getMsgDate(win),
+ };
+ const cApi = lazy.EnigmailCryptoAPI();
+
+ // ensure all lines end with CRLF as specified in RFC 3156, section 5
+ if (this.signedData.search(/[^\r]\n/) >= 0) {
+ this.signedData = this.signedData
+ .replace(/\r\n/g, "\n")
+ .replace(/\n/g, "\r\n");
+ }
+
+ this.returnStatus = cApi.sync(cApi.verifyMime(this.signedData, options));
+
+ if (!this.returnStatus) {
+ this.exitCode = -1;
+ } else {
+ this.exitCode = this.returnStatus.exitCode;
+
+ this.returnStatus.statusFlags |= EnigmailConstants.PGP_MIME_SIGNED;
+
+ if (this.partiallySigned) {
+ this.returnStatus.statusFlags |= EnigmailConstants.PARTIALLY_PGP;
+ }
+
+ this.displayStatus();
+ }
+ }
+ },
+
+ // return data to libMime
+ returnData(data) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: returnData: " + data.length + " bytes\n"
+ );
+
+ let m = data.match(/^(content-type: +)([\w/]+)/im);
+ if (m && m.length >= 3) {
+ let contentType = m[2];
+ if (contentType.search(/^text/i) === 0) {
+ // add multipart/mixed boundary to work around TB bug (empty forwarded message)
+ let bound = lazy.EnigmailMime.createBoundary();
+ data =
+ 'Content-Type: multipart/mixed; boundary="' +
+ bound +
+ '"\n' +
+ "Content-Disposition: inline\n\n--" +
+ bound +
+ "\n" +
+ data +
+ "\n--" +
+ bound +
+ "--\n";
+ }
+ }
+
+ this.mimeSvc.outputDecryptedData(data, data.length);
+ },
+
+ setWindow(window, msgUriSpec) {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: setWindow: " + msgUriSpec + "\n");
+
+ if (!this.window) {
+ this.window = window;
+ this.msgUriSpec = msgUriSpec;
+ }
+ },
+
+ displayStatus() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: displayStatus\n");
+ if (
+ this.exitCode === null ||
+ this.window === null ||
+ this.statusDisplayed ||
+ this.backgroundJob
+ ) {
+ return;
+ }
+
+ try {
+ LOCAL_DEBUG("mimeVerify.jsm: displayStatus displaying result\n");
+ let headerSink = lazy.EnigmailSingletons.messageReader;
+
+ if (this.protectedHeaders) {
+ headerSink.processDecryptionResult(
+ this.uri,
+ "modifyMessageHeaders",
+ JSON.stringify(this.protectedHeaders.newHeaders),
+ this.mimePartNumber
+ );
+ }
+
+ if (headerSink) {
+ headerSink.updateSecurityStatus(
+ this.lastMsgUri,
+ this.exitCode,
+ this.returnStatus.statusFlags,
+ this.returnStatus.extStatusFlags,
+ this.returnStatus.keyId,
+ this.returnStatus.userId,
+ this.returnStatus.sigDetails,
+ this.returnStatus.errorMsg,
+ this.returnStatus.blockSeparation,
+ this.uri,
+ JSON.stringify({
+ encryptedTo: this.returnStatus.encToDetails,
+ }),
+ this.mimePartNumber
+ );
+ }
+ this.statusDisplayed = true;
+ } catch (ex) {
+ lazy.EnigmailLog.writeException("mimeVerify.jsm", ex);
+ }
+ },
+};
+
+////////////////////////////////////////////////////////////////////
+// General-purpose functions, not exported
+
+function LOCAL_DEBUG(str) {
+ if (gDebugLog) {
+ lazy.EnigmailLog.DEBUG(str);
+ }
+}