# HG changeset patch # User Paul Boddie # Date 1350599617 -7200 # Node ID cbed7abb04da15c4bec1ccb5a413a667a266ced3 # Parent 4dea36b3c060444f3528013e0de1c5d1263c39eb Moved decryption and verification support into MoinMessage. Introduced a GPG class to hold common configuration information and provide a more natural programming interface. Added a setup script. diff -r 4dea36b3c060 -r cbed7abb04da MoinMessage.py --- a/MoinMessage.py Sun Jul 22 02:00:31 2012 +0200 +++ b/MoinMessage.py Fri Oct 19 00:33:37 2012 +0200 @@ -6,7 +6,6 @@ @license: GNU GPL (v2 or later), see COPYING.txt for details. """ -from MoinMoin.log import getLogger from email import message_from_string from email.encoders import encode_noop from email.mime.multipart import MIMEMultipart @@ -14,7 +13,9 @@ from email.mime.base import MIMEBase from email.mime.text import MIMEText from subprocess import Popen, PIPE +from tempfile import mkstemp import httplib +import os class Message: @@ -46,76 +47,129 @@ class MoinMessageError(Exception): pass -def gpg(args, text): +class GPG: + + "A wrapper around the gpg command using a particular configuration." - "Invoke gpg with the given 'args', supplying the given 'text'." + def __init__(self, homedir=None): + self.conf_args = [] - cmd = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + if homedir: + self.conf_args += ["--homedir", homedir] + + self.errors = None - cmd.stdin.write(text) - cmd.stdin.close() + def run(self, args, text=None): - errors = cmd.stderr.read() - if errors: - getLogger(__name__).warning(errors) + """ + Invoke gpg with the given 'args', supplying the given 'text' to the + command directly or, if 'text' is omitted, using a file provided as part + of the 'args' if appropriate. - try: - text = cmd.stdout.read() + Failure to complete the operation will result in a MoinMessageError + being raised. + """ + + cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE) - # Test for a zero result. + if text: + cmd.stdin.write(text) + cmd.stdin.close() - if not cmd.wait(): - return text - else: - raise MoinMessageError, errors + self.errors = cmd.stderr.read() + + try: + text = cmd.stdout.read() + + # Test for a zero result. - finally: - cmd.stdout.close() - cmd.stderr.close() + if not cmd.wait(): + return text + else: + raise MoinMessageError, errors + + finally: + cmd.stdout.close() + cmd.stderr.close() -def signMessage(message, keyid): + def verifyMessage(self, signature, content): + + "Using the given 'signature', verify the given message 'content'." - """ - Return a signed 'message' using the given 'keyid'. - """ + # Write the detached signature and content to files. + + signature_fd, signature_filename = mkstemp() + content_fd, content_filename = mkstemp() - text = message.as_string() - signature = gpg(["gpg", "--armor", "-u", keyid, "--detach-sig"], text) + try: + signature_fp = os.fdopen(signature_fd, "w") + content_fp = os.fdopen(content_fd, "w") + try: + signature_fp.write(signature) + content_fp.write(content) + finally: + signature_fp.close() + content_fp.close() - # Make the container for the message. + # Verify the message text. - signed_message = MIMEMultipart("signed", protocol="application/pgp-signature") - signed_message.attach(message) + self.run(["--verify", signature_filename, content_filename]) - signature_part = MIMEBase("application", "pgp-signature") - signature_part.set_payload(signature) - signed_message.attach(signature_part) + finally: + os.remove(signature_filename) + os.remove(content_filename) + + def signMessage(self, message, keyid): - return signed_message + """ + Return a signed version of 'message' using the given 'keyid'. + """ -def encryptMessage(message, keyid): + text = message.as_string() + signature = self.run(["--armor", "-u", keyid, "--detach-sig"], text) + + # Make the container for the message. + + signed_message = MIMEMultipart("signed", protocol="application/pgp-signature") + signed_message.attach(message) - """ - Return an encrypted 'message' using the given 'keyid'. - """ + signature_part = MIMEBase("application", "pgp-signature") + signature_part.set_payload(signature) + signed_message.attach(signature_part) + + return signed_message + + def decryptMessage(self, message): - text = message.as_string() - encrypted = gpg(["gpg", "--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text) + "Return a decrypted version of 'message'." + + return self.run(["--decrypt"], message) - # Make the container for the message. + def encryptMessage(self, message, keyid): - encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") + """ + Return an encrypted version of 'message' using the given 'keyid'. + """ - # For encrypted content, add the declaration and content. + text = message.as_string() + encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text) + + # Make the container for the message. + + encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") + + # For encrypted content, add the declaration and content. - declaration = MIMEBase("application", "pgp-encrypted") - declaration.set_payload("Version: 1") - encrypted_message.attach(declaration) + declaration = MIMEBase("application", "pgp-encrypted") + declaration.set_payload("Version: 1") + encrypted_message.attach(declaration) - content = MIMEApplication(encrypted, "octet-stream", encode_noop) - encrypted_message.attach(content) + content = MIMEApplication(encrypted, "octet-stream", encode_noop) + encrypted_message.attach(content) - return encrypted_message + return encrypted_message + +# Communications functions. def sendMessage(message, host, path): diff -r 4dea36b3c060 -r cbed7abb04da actions/PostMessage.py --- a/actions/PostMessage.py Sun Jul 22 02:00:31 2012 +0200 +++ b/actions/PostMessage.py Fri Oct 19 00:33:37 2012 +0200 @@ -9,10 +9,8 @@ from MoinMoin.PageEditor import PageEditor from MoinMoin.log import getLogger from MoinSupport import * +from MoinMessage import GPG, MoinMessageError from email.parser import Parser -from subprocess import Popen, PIPE -from tempfile import mkstemp -import os try: from cStringIO import StringIO @@ -102,37 +100,27 @@ if not homedir: return - cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"], - stdin=PIPE, stdout=PIPE, stderr=PIPE) - - cmd.stdin.write(content.get_payload()) - cmd.stdin.close() + gpg = GPG(homedir) - errors = cmd.stderr.read() - if errors: - getLogger(__name__).warning(errors) - - # Handle the embedded message. + # Get the decrypted message text. try: - # Get the decrypted message text. - - text = cmd.stdout.read() + text = gpg.decryptMessage(content.get_payload()) - # With a zero return code, accept the message. + # Log non-fatal errors. - if not cmd.wait(): - self.handle_message_text(text) + if gpg.errors: + getLogger(__name__).warning(gpg.errors) - # Otherwise, reject the unverified message. + # Handle the embedded message. + + self.handle_message_text(text) - else: - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") - request.write("The message could not be decrypted.") + # Otherwise, reject the unverified message. - finally: - cmd.stdout.close() - cmd.stderr.close() + except MoinMessageError: + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") + request.write("The message could not be decrypted.") def handle_signed_message(self, message): @@ -163,49 +151,27 @@ if not homedir: return - # Write the detached signature and content to files. + gpg = GPG(homedir) - signature_fd, signature_filename = mkstemp() - content_fd, content_filename = mkstemp() + # Verify the message. + try: - signature_fp = os.fdopen(signature_fd, "w") - content_fp = os.fdopen(content_fd, "w") - try: - signature_fp.write(signature.get_payload()) - content_fp.write(content.as_string()) - finally: - signature_fp.close() - content_fp.close() + gpg.verifyMessage(signature.get_payload(), content.as_string()) - # Verify the message text. + # Log non-fatal errors. - cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename], - stderr=PIPE) - - errors = cmd.stderr.read() - if errors: - getLogger(__name__).warning(errors) + if gpg.errors: + getLogger(__name__).warning(gpg.errors) # Handle the embedded message. - try: - # With a zero return code, accept the message. + self.handle_message_content(content) - if not cmd.wait(): - self.handle_message_content(content) - - # Otherwise, reject the unverified message. + # Otherwise, reject the unverified message. - else: - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") - request.write("The message could not be verified.") - - finally: - cmd.stderr.close() - - finally: - os.remove(signature_filename) - os.remove(content_filename) + except MoinMessageError: + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") + request.write("The message could not be verified.") def handle_message_content(self, message): diff -r 4dea36b3c060 -r cbed7abb04da setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Fri Oct 19 00:33:37 2012 +0200 @@ -0,0 +1,13 @@ +#! /usr/bin/env python + +from distutils.core import setup + +setup( + name = "MoinMessage", + description = "Send and receive GPG/PGP-signed/encrypted messages using MoinMoin", + author = "Paul Boddie", + author_email = "paul@boddie.org.uk", + url = "http://hgweb.boddie.org.uk/MoinMessage", + version = "0.1", + py_modules = ["MoinMessage"] + ) diff -r 4dea36b3c060 -r cbed7abb04da tests/test_send.py --- a/tests/test_send.py Sun Jul 22 02:00:31 2012 +0200 +++ b/tests/test_send.py Fri Oct 19 00:33:37 2012 +0200 @@ -14,10 +14,11 @@ message.add_update([MIMEText("An update to the Wiki.", "moin")]) message.add_update([MIMEText("Another update to the Wiki.", "moin")]) email_message = message.get_payload() + gpg = GPG() try: - signed_message = signMessage(email_message, signer) - encrypted_message = encryptMessage(signed_message, recipient) + signed_message = gpg.signMessage(email_message, signer) + encrypted_message = gpg.encryptMessage(signed_message, recipient) print sendMessage(encrypted_message, host, path) except MoinMessageError, exc: print exc