Add proper Attachment handling for MIME messages.
authorAndre Heinecke <aheinecke@intevation.de>
Mon, 21 Sep 2015 13:06:57 +0000 (15:06 +0200)
committerAndre Heinecke <aheinecke@intevation.de>
Mon, 21 Sep 2015 13:18:57 +0000 (15:18 +0200)
Attachments are now session decrypted in the read event and
session encrypted again in the write event.

* src/attachment.cpp, src/attachment.h: Functions for attachment
Session encryption / decryption.
* src/Makefile.am: Add new files.
* src/mailitem-events.cpp (~MailItemEvents): Resolve Macro
to avoid confusing bracing.
(MailItemEvents::handle_read): Remove session encryption from
attachments.
(MailItemEvents::invoke): Track "wipe" status. Session encrypt
attachments in write event.
* src/mapihelp.cpp (get_gpgolattachtype): Unstatic.
* src/mapihelp.h (get_gpgolattachtype): Expose.
(attachtype_t): Define attachment type for temporary unprotected
Attachments.

--

The code will probably be changed to just untprotect an attachment
when it is accessed in the BeforeAttachmentWriteToTempFile event.

src/Makefile.am
src/attachment.cpp [new file with mode: 0644]
src/attachment.h [new file with mode: 0644]
src/mailitem-events.cpp
src/mapihelp.cpp
src/mapihelp.h

index c983dcc..71246a3 100644 (file)
@@ -71,7 +71,6 @@ gpgol_SOURCES = \
        mapihelp.cpp mapihelp.h     \
        mymapi.h  mymapitags.h      \
        serpent.c serpent.h         \
-        vasprintf.c                 \
        ext-commands.cpp ext-commands.h       \
        user-events.cpp     user-events.h     \
        session-events.cpp  session-events.h  \
@@ -90,7 +89,8 @@ gpgol_SOURCES = \
        parsetlv.c parsetlv.h \
        filetype.c filetype.h \
        eventsinks.h application-events.cpp \
-       mailitem-events.cpp
+       mailitem-events.cpp \
+       attachment.h attachment.cpp
 
 
 #treeview_SOURCES = treeview.c
diff --git a/src/attachment.cpp b/src/attachment.cpp
new file mode 100644 (file)
index 0000000..9ecd970
--- /dev/null
@@ -0,0 +1,324 @@
+/* attachment.cpp - Functions for attachment handling
+ *    Copyright (C) 2005, 2007 g10 Code GmbH
+ *    Copyright (C) 2015 Intevation GmbH
+ *
+ * This file is part of GpgOL.
+ *
+ * GpgOL is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GpgOL is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "common.h"
+#include "attachment.h"
+#include "serpent.h"
+#include "oomhelp.h"
+#include "mymapitags.h"
+#include "mapihelp.h"
+
+#include <objidlbase.h>
+
+#define COPYBUFFERSIZE 4096
+
+#define IV_DEFAULT_LEN 16
+
+/** Decrypt the first 16 bytes of stream and check that it contains
+  our header. Return 0 on success. */
+static int
+check_header (LPSTREAM stream, symenc_t symenc)
+{
+  HRESULT hr;
+  char tmpbuf[16];
+  ULONG nread;
+  hr = stream->Read (tmpbuf, 16, &nread);
+  if (hr || nread != 16)
+    {
+      log_error ("%s:%s: Read failed: hr=%#lx", SRCNAME, __func__, hr);
+      return -1;
+    }
+  symenc_cfb_decrypt (symenc, tmpbuf, tmpbuf, 16);
+  if (memcmp (tmpbuf, "GpgOL attachment", 16))
+    {
+      log_error ("%s:%s: Invalid header.",
+                 SRCNAME, __func__);
+      char buf2 [17];
+      snprintf (buf2, 17, "%s", tmpbuf);
+      log_error("Buf2: %s", buf2);
+      return -1;
+    }
+  return 0;
+}
+
+/** Encrypts or decrypts a stream in place using the symenc context.
+  Returns 0 on success. */
+static int
+do_crypt_stream (LPSTREAM stream, symenc_t symenc, bool encrypt)
+{
+  char *buf = NULL;
+  HRESULT hr;
+  ULONG nread;
+  bool fixed_str_written = false;
+  int rc = -1;
+  ULONG written = 0;
+  /* The original intention was to use IStream::Clone to have
+     an independent read / write stream. But the MAPI attachment
+     stream returns E_NOT_IMPLMENTED for that :-)
+     So we manually track the read and writepos. Read is offset
+     at 16 because of the GpgOL message. */
+  LARGE_INTEGER readpos = {0},
+                writepos = {0};
+
+  if (!encrypt)
+    {
+      readpos.QuadPart = 16;
+    }
+
+  buf = (char*)xmalloc (COPYBUFFERSIZE);
+  do
+    {
+      hr = stream->Read (buf, COPYBUFFERSIZE, &nread);
+      if (hr)
+        {
+          log_error ("%s:%s: Read failed: hr=%#lx", SRCNAME, __func__, hr);
+          goto done;
+        }
+      if (!nread)
+        {
+          break;
+        }
+      readpos.QuadPart += nread;
+      stream->Seek(writepos, STREAM_SEEK_SET, NULL);
+      if (nread && encrypt && !fixed_str_written)
+        {
+          char tmpbuf[16];
+          /* Write an encrypted fixed 16 byte string which we need to
+             check at decryption time to see whether we have actually
+             encrypted it using this session key.  */
+          symenc_cfb_encrypt (symenc, tmpbuf, "GpgOL attachment", 16);
+          stream->Write (tmpbuf, 16, NULL);
+          fixed_str_written = true;
+          writepos.QuadPart = 16;
+        }
+      if (encrypt)
+        {
+          symenc_cfb_encrypt (symenc, buf, buf, nread);
+        }
+      else
+        {
+          symenc_cfb_decrypt (symenc, buf, buf, nread);
+        }
+
+        hr = stream->Write (buf, nread, &written);
+        if (FAILED (hr) || written != nread)
+          {
+            log_error ("%s:%s: Write failed: %i", SRCNAME, __func__, __LINE__);
+            goto done;
+          }
+        writepos.QuadPart += written;
+        stream->Seek(readpos, STREAM_SEEK_SET, NULL);
+      }
+    while (nread == COPYBUFFERSIZE);
+  rc = 0;
+
+done:
+  xfree (buf);
+
+  if (rc)
+    {
+      stream->Revert ();
+    }
+  else
+    {
+      stream->Commit (0);
+    }
+
+  return rc;
+}
+
+/** If encrypt is set to true this will encrypt the attachment
+  data with serpent otherwiese it will decrypt.
+  This function handles the mapi side of things.
+  */
+static int
+do_crypt_mapi (LPATTACH att, bool encrypt)
+{
+  char *iv;
+  ULONG tag;
+  size_t ivlen = IV_DEFAULT_LEN;
+  symenc_t symenc = NULL;
+  HRESULT hr;
+  LPSTREAM stream = NULL;
+  int rc = -1;
+
+  if (!att)
+    {
+      log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+      return -1;
+    }
+
+  /* Get or create a new IV */
+  if (get_gpgolprotectiv_tag ((LPMESSAGE)att, &tag) )
+    {
+      log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+      return -1;
+    }
+  if (encrypt)
+    {
+      iv = (char*)create_initialization_vector (IV_DEFAULT_LEN);
+    }
+  else
+    {
+      iv = mapi_get_binary_prop ((LPMESSAGE)att, tag, &ivlen);
+    }
+  if (!iv)
+    {
+      log_error ("%s:%s: Error creating / getting IV: %i", SRCNAME,
+                 __func__, __LINE__);
+      goto done;
+    }
+
+  symenc = symenc_open (get_128bit_session_key (), 16, iv, ivlen);
+  xfree (iv);
+  if (!symenc)
+    {
+      log_error ("%s:%s: can't open encryption context", SRCNAME, __func__);
+      goto done;
+    }
+
+  hr = att->OpenProperty (PR_ATTACH_DATA_BIN, &IID_IStream,
+                          0, MAPI_MODIFY, (LPUNKNOWN*) &stream);
+  if (FAILED (hr))
+    {
+      log_error ("%s:%s: can't open data stream of attachment: hr=%#lx",
+                 SRCNAME, __func__, hr);
+      goto done;
+    }
+
+  /* When decrypting check the first 16 bytes for the header */
+  if (!encrypt && check_header (stream, symenc))
+    {
+      goto done;
+    }
+
+  if (FAILED (hr))
+    {
+      log_error ("%s:%s: can't create temp file: hr=%#lx",
+                 SRCNAME, __func__, hr);
+      goto done;
+    }
+
+  if (do_crypt_stream (stream, symenc, encrypt))
+    {
+      log_error ("%s:%s: stream handling failed",
+                 SRCNAME, __func__);
+      goto done;
+    }
+  rc = 0;
+
+done:
+  if (symenc)
+    symenc_close (symenc);
+  RELDISP (stream);
+
+  return rc;
+}
+
+/** Protect or unprotect attachments.*/
+static int
+do_crypt (LPDISPATCH mailitem, bool protect)
+{
+  LPDISPATCH attachments = get_oom_object (mailitem, "Attachments");
+  LPMESSAGE message = get_oom_base_message (mailitem);
+  int count = 0;
+  int err = -1;
+  char *item_str;
+  int i;
+  ULONG tag_id;
+
+  if (!attachments || !message)
+    {
+      log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+      return -1;
+    }
+  count = get_oom_int (attachments, "Count");
+
+  if (get_gpgolattachtype_tag (message, &tag_id))
+    {
+      log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+      goto done;
+    }
+
+  if (count < 1)
+    {
+      log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+      goto done;
+    }
+
+  /* Yes the items start at 1! */
+  for (i = 1; i <= count; i++)
+    {
+      LPDISPATCH attachment;
+      LPATTACH mapi_attachment;
+      attachtype_t att_type;
+
+      if (asprintf (&item_str, "Item(%i)", i) == -1)
+        {
+          log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+          goto done;
+        }
+
+      attachment = get_oom_object (attachments, item_str);
+      xfree (item_str);
+      if (!attachment)
+        {
+          log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
+        }
+      mapi_attachment = (LPATTACH) get_oom_iunknown (attachment,
+                                                     "MapiObject");
+
+      att_type = get_gpgolattachtype (mapi_attachment, tag_id);
+      if ((protect && att_type == ATTACHTYPE_FROMMOSS_DEC) ||
+          (!protect && att_type == ATTACHTYPE_FROMMOSS))
+        {
+          if (do_crypt_mapi (mapi_attachment, protect))
+            {
+              log_error ("%s:%s: Error: Session crypto failed.",
+                         SRCNAME, __func__);
+              mapi_attachment->Release ();
+              attachment->Release ();
+              goto done;
+            }
+        }
+      mapi_attachment->Release ();
+      attachment->Release ();
+    }
+  err = 0;
+
+done:
+
+  RELDISP (message);
+  RELDISP (attachments);
+  return err;
+}
+
+int
+protect_attachments (LPDISPATCH mailitem)
+{
+  return do_crypt (mailitem, true);
+}
+
+int
+unprotect_attachments (LPDISPATCH mailitem)
+{
+  return do_crypt (mailitem, false);
+}
diff --git a/src/attachment.h b/src/attachment.h
new file mode 100644 (file)
index 0000000..932a21a
--- /dev/null
@@ -0,0 +1,52 @@
+/* attachment.h - Functions for attachment handling
+ *    Copyright (C) 2005, 2007 g10 Code GmbH
+ *    Copyright (C) 2015 Intevation GmbH
+ *
+ * This file is part of GpgOL.
+ *
+ * GpgOL is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GpgOL is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+#ifndef ATTACHMENT_H
+#define ATTACHMENT_H
+
+#include <windows.h>
+
+/** Protect attachments so that it can be stored
+  by outlook. This means to symetrically encrypt the
+  data with the session key.
+
+  This will change the messagetype back to
+  ATTACHTYPE_FROMMOSS it is only supposed to be
+  called on attachments with the Attachmentype
+  ATTACHTYPE_FROMMOSS_DEC.
+
+  The dispatch paramenter should be a mailitem.
+
+  Returns 0 on success.
+*/
+int
+protect_attachments (LPDISPATCH mailitem);
+
+/** Remove the symetric session encryption of the attachments.
+
+  The dispatch paramenter should be a mailitem.
+
+  This will change the messsagetype to
+  ATTACHTYPE_FROMMOSS_DEC it should only be called
+  with attachments of the type ATTACHTYPE_FROMMOSS.
+
+  Returns 0 on success. */
+int
+unprotect_attachments (LPDISPATCH mailitem);
+#endif // ATTACHMENT_H
index 98f6385..34ac116 100644 (file)
@@ -24,6 +24,9 @@
 #include "message.h"
 #include "oomhelp.h"
 #include "ocidl.h"
+#include "attachment.h"
+#include "mapihelp.h"
+
 
 typedef enum
   {
@@ -63,6 +66,7 @@ private:
   bool m_send_seen,   /* The message is about to be submitted */
        m_want_html,    /* Encryption of HTML is desired. */
        m_processed,    /* The message has been porcessed by us.  */
+       m_needs_wipe,   /* We have added plaintext to the mesage. */
        m_was_encrypted; /* The original message was encrypted.  */
 
   HRESULT handle_before_read();
@@ -79,9 +83,15 @@ MailItemEvents::MailItemEvents() :
     m_want_html(false),
     m_processed(false)
 {
-/* The event sink default dtor closes this for us. */
-EVENT_SINK_DEFAULT_DTOR(MailItemEvents)
+}
 
+MailItemEvents::~MailItemEvents()
+{
+  if (m_pCP)
+    m_pCP->Unadvise(m_cookie);
+  if (m_object)
+    m_object->Release();
+}
 
 HRESULT
 MailItemEvents::handle_read()
@@ -113,6 +123,12 @@ MailItemEvents::handle_read()
 
   xfree (body);
 
+  if (unprotect_attachments (m_object))
+    {
+      log_error ("%s:%s: Failed to unprotect attachments. \n",
+                 SRCNAME, __func__);
+    }
+
   return S_OK;
 }
 
@@ -139,8 +155,10 @@ MailItemEvents::handle_before_read()
   log_debug ("%s:%s: incoming handler status: %i",
              SRCNAME, __func__, err);
   message->Release ();
+  return S_OK;
 }
 
+
 HRESULT
 MailItemEvents::handle_after_write()
 {
@@ -170,6 +188,14 @@ MailItemEvents::handle_after_write()
   return S_OK;
 }
 
+/* The main Invoke function. The return value of this
+   function does not appear to have any effect on outlook
+   although I have read in an example somewhere that you
+   should return S_OK so that outlook continues to handle
+   the event I have not yet seen any effect by returning
+   error values here and no MSDN documentation about the
+   return values.
+*/
 EVENT_SINK_INVOKE(MailItemEvents)
 {
   USE_INVOKE_ARGS
@@ -183,7 +209,8 @@ EVENT_SINK_INVOKE(MailItemEvents)
         {
           if (m_processed)
             {
-              return handle_read();
+              m_needs_wipe = m_was_encrypted;
+              handle_read();
             }
           return S_OK;
         }
@@ -202,16 +229,39 @@ EVENT_SINK_INVOKE(MailItemEvents)
         }
       case Write:
         {
-          if (m_wasencrypted && m_processed && !m_send_seen)
+          /* This is a bit strange. We sometimes get multiple write events
+             without a read in between. When we access the message in
+             the second event it fails and if we cancel the event outlook
+             crashes. So we have keep the m_needs_wipe state variable
+             to keep track of that. */
+          if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF))
+           {
+             /* This happens in the weird case */
+             log_oom ("%s:%s: Uncancellable write event.",
+                      SRCNAME, __func__);
+             break;
+           }
+          if (m_processed && m_needs_wipe && !m_send_seen)
             {
-              log_debug ("%s:%s: Wiping plaintext from body of Message: %lx \n",
-                         SRCNAME, __func__, m_object, dispid);
-              put_oom_string (m_object, "HTMLBody", "");
-              put_oom_string (m_object, "Body", "");
+              log_debug ("%s:%s: Message %p removing plaintext from Message.",
+                         SRCNAME, __func__, m_object);
+              if (put_oom_string (m_object, "HTMLBody", "") ||
+                  put_oom_string (m_object, "Body", "") ||
+                  protect_attachments (m_object))
+                {
+                  /* An error cleaning the mail should not happen normally.
+                     But just in case there is an error we cancel the
+                     write here. */
+                  log_debug ("%s:%s: Failed to remove plaintext.",
+                             SRCNAME, __func__);
+                  *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE;
+                  return E_ABORT;
+                }
+              m_needs_wipe = false;
             }
         }
       default:
-        log_debug ("%s:%s: Message:%lx Unhandled Event: %lx \n",
+        log_oom_extra ("%s:%s: Message:%p Unhandled Event: %lx \n",
                        SRCNAME, __func__, m_object, dispid);
     }
   return S_OK;
index 1d6d7c5..81b3fd2 100644 (file)
@@ -1852,7 +1852,7 @@ get_attach_mime_tag (LPATTACH obj)
 
 /* Return the GpgOL Attach Type for attachment OBJ.  Tag needs to be
    the tag of that property. */
-static attachtype_t
+attachtype_t
 get_gpgolattachtype (LPATTACH obj, ULONG tag)
 {
   HRESULT hr;
index 05417c6..8173b94 100644 (file)
@@ -50,9 +50,13 @@ typedef enum
     ATTACHTYPE_FROMMOSS = 2,     /* Attachment created from MOSS.  */
     ATTACHTYPE_MOSSTEMPL = 3,    /* Attachment has been created in the
                                     course of sending a message */ 
-    ATTACHTYPE_PGPBODY = 4       /* Attachment contains the original
+    ATTACHTYPE_PGPBODY = 4,      /* Attachment contains the original
                                     PGP message body of PGP inline
                                     encrypted messages.  */
+    ATTACHTYPE_FROMMOSS_DEC = 5  /* A FROMMOSS attachment that has been
+                                    temporarily decrypted and needs to be
+                                    encrypted before it is written back
+                                    into storage. */
   }
 attachtype_t;
 
@@ -167,6 +171,7 @@ int   mapi_delete_gpgol_body_attachment (LPMESSAGE message);
 
 int   mapi_attachment_to_body (LPMESSAGE message, mapi_attach_item_t *item);
 
+attachtype_t get_gpgolattachtype (LPATTACH obj, ULONG tag);
 #ifdef __cplusplus
 }
 #endif