1.1 --- a/MoinMessage.py Sun Jul 22 02:00:31 2012 +0200
1.2 +++ b/MoinMessage.py Fri Oct 19 00:33:37 2012 +0200
1.3 @@ -6,7 +6,6 @@
1.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.5 """
1.6
1.7 -from MoinMoin.log import getLogger
1.8 from email import message_from_string
1.9 from email.encoders import encode_noop
1.10 from email.mime.multipart import MIMEMultipart
1.11 @@ -14,7 +13,9 @@
1.12 from email.mime.base import MIMEBase
1.13 from email.mime.text import MIMEText
1.14 from subprocess import Popen, PIPE
1.15 +from tempfile import mkstemp
1.16 import httplib
1.17 +import os
1.18
1.19 class Message:
1.20
1.21 @@ -46,76 +47,129 @@
1.22 class MoinMessageError(Exception):
1.23 pass
1.24
1.25 -def gpg(args, text):
1.26 +class GPG:
1.27 +
1.28 + "A wrapper around the gpg command using a particular configuration."
1.29
1.30 - "Invoke gpg with the given 'args', supplying the given 'text'."
1.31 + def __init__(self, homedir=None):
1.32 + self.conf_args = []
1.33
1.34 - cmd = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
1.35 + if homedir:
1.36 + self.conf_args += ["--homedir", homedir]
1.37 +
1.38 + self.errors = None
1.39
1.40 - cmd.stdin.write(text)
1.41 - cmd.stdin.close()
1.42 + def run(self, args, text=None):
1.43
1.44 - errors = cmd.stderr.read()
1.45 - if errors:
1.46 - getLogger(__name__).warning(errors)
1.47 + """
1.48 + Invoke gpg with the given 'args', supplying the given 'text' to the
1.49 + command directly or, if 'text' is omitted, using a file provided as part
1.50 + of the 'args' if appropriate.
1.51
1.52 - try:
1.53 - text = cmd.stdout.read()
1.54 + Failure to complete the operation will result in a MoinMessageError
1.55 + being raised.
1.56 + """
1.57 +
1.58 + cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE)
1.59
1.60 - # Test for a zero result.
1.61 + if text:
1.62 + cmd.stdin.write(text)
1.63 + cmd.stdin.close()
1.64
1.65 - if not cmd.wait():
1.66 - return text
1.67 - else:
1.68 - raise MoinMessageError, errors
1.69 + self.errors = cmd.stderr.read()
1.70 +
1.71 + try:
1.72 + text = cmd.stdout.read()
1.73 +
1.74 + # Test for a zero result.
1.75
1.76 - finally:
1.77 - cmd.stdout.close()
1.78 - cmd.stderr.close()
1.79 + if not cmd.wait():
1.80 + return text
1.81 + else:
1.82 + raise MoinMessageError, errors
1.83 +
1.84 + finally:
1.85 + cmd.stdout.close()
1.86 + cmd.stderr.close()
1.87
1.88 -def signMessage(message, keyid):
1.89 + def verifyMessage(self, signature, content):
1.90 +
1.91 + "Using the given 'signature', verify the given message 'content'."
1.92
1.93 - """
1.94 - Return a signed 'message' using the given 'keyid'.
1.95 - """
1.96 + # Write the detached signature and content to files.
1.97 +
1.98 + signature_fd, signature_filename = mkstemp()
1.99 + content_fd, content_filename = mkstemp()
1.100
1.101 - text = message.as_string()
1.102 - signature = gpg(["gpg", "--armor", "-u", keyid, "--detach-sig"], text)
1.103 + try:
1.104 + signature_fp = os.fdopen(signature_fd, "w")
1.105 + content_fp = os.fdopen(content_fd, "w")
1.106 + try:
1.107 + signature_fp.write(signature)
1.108 + content_fp.write(content)
1.109 + finally:
1.110 + signature_fp.close()
1.111 + content_fp.close()
1.112
1.113 - # Make the container for the message.
1.114 + # Verify the message text.
1.115
1.116 - signed_message = MIMEMultipart("signed", protocol="application/pgp-signature")
1.117 - signed_message.attach(message)
1.118 + self.run(["--verify", signature_filename, content_filename])
1.119
1.120 - signature_part = MIMEBase("application", "pgp-signature")
1.121 - signature_part.set_payload(signature)
1.122 - signed_message.attach(signature_part)
1.123 + finally:
1.124 + os.remove(signature_filename)
1.125 + os.remove(content_filename)
1.126 +
1.127 + def signMessage(self, message, keyid):
1.128
1.129 - return signed_message
1.130 + """
1.131 + Return a signed version of 'message' using the given 'keyid'.
1.132 + """
1.133
1.134 -def encryptMessage(message, keyid):
1.135 + text = message.as_string()
1.136 + signature = self.run(["--armor", "-u", keyid, "--detach-sig"], text)
1.137 +
1.138 + # Make the container for the message.
1.139 +
1.140 + signed_message = MIMEMultipart("signed", protocol="application/pgp-signature")
1.141 + signed_message.attach(message)
1.142
1.143 - """
1.144 - Return an encrypted 'message' using the given 'keyid'.
1.145 - """
1.146 + signature_part = MIMEBase("application", "pgp-signature")
1.147 + signature_part.set_payload(signature)
1.148 + signed_message.attach(signature_part)
1.149 +
1.150 + return signed_message
1.151 +
1.152 + def decryptMessage(self, message):
1.153
1.154 - text = message.as_string()
1.155 - encrypted = gpg(["gpg", "--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text)
1.156 + "Return a decrypted version of 'message'."
1.157 +
1.158 + return self.run(["--decrypt"], message)
1.159
1.160 - # Make the container for the message.
1.161 + def encryptMessage(self, message, keyid):
1.162
1.163 - encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
1.164 + """
1.165 + Return an encrypted version of 'message' using the given 'keyid'.
1.166 + """
1.167
1.168 - # For encrypted content, add the declaration and content.
1.169 + text = message.as_string()
1.170 + encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text)
1.171 +
1.172 + # Make the container for the message.
1.173 +
1.174 + encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
1.175 +
1.176 + # For encrypted content, add the declaration and content.
1.177
1.178 - declaration = MIMEBase("application", "pgp-encrypted")
1.179 - declaration.set_payload("Version: 1")
1.180 - encrypted_message.attach(declaration)
1.181 + declaration = MIMEBase("application", "pgp-encrypted")
1.182 + declaration.set_payload("Version: 1")
1.183 + encrypted_message.attach(declaration)
1.184
1.185 - content = MIMEApplication(encrypted, "octet-stream", encode_noop)
1.186 - encrypted_message.attach(content)
1.187 + content = MIMEApplication(encrypted, "octet-stream", encode_noop)
1.188 + encrypted_message.attach(content)
1.189
1.190 - return encrypted_message
1.191 + return encrypted_message
1.192 +
1.193 +# Communications functions.
1.194
1.195 def sendMessage(message, host, path):
1.196
2.1 --- a/actions/PostMessage.py Sun Jul 22 02:00:31 2012 +0200
2.2 +++ b/actions/PostMessage.py Fri Oct 19 00:33:37 2012 +0200
2.3 @@ -9,10 +9,8 @@
2.4 from MoinMoin.PageEditor import PageEditor
2.5 from MoinMoin.log import getLogger
2.6 from MoinSupport import *
2.7 +from MoinMessage import GPG, MoinMessageError
2.8 from email.parser import Parser
2.9 -from subprocess import Popen, PIPE
2.10 -from tempfile import mkstemp
2.11 -import os
2.12
2.13 try:
2.14 from cStringIO import StringIO
2.15 @@ -102,37 +100,27 @@
2.16 if not homedir:
2.17 return
2.18
2.19 - cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"],
2.20 - stdin=PIPE, stdout=PIPE, stderr=PIPE)
2.21 -
2.22 - cmd.stdin.write(content.get_payload())
2.23 - cmd.stdin.close()
2.24 + gpg = GPG(homedir)
2.25
2.26 - errors = cmd.stderr.read()
2.27 - if errors:
2.28 - getLogger(__name__).warning(errors)
2.29 -
2.30 - # Handle the embedded message.
2.31 + # Get the decrypted message text.
2.32
2.33 try:
2.34 - # Get the decrypted message text.
2.35 -
2.36 - text = cmd.stdout.read()
2.37 + text = gpg.decryptMessage(content.get_payload())
2.38
2.39 - # With a zero return code, accept the message.
2.40 + # Log non-fatal errors.
2.41
2.42 - if not cmd.wait():
2.43 - self.handle_message_text(text)
2.44 + if gpg.errors:
2.45 + getLogger(__name__).warning(gpg.errors)
2.46
2.47 - # Otherwise, reject the unverified message.
2.48 + # Handle the embedded message.
2.49 +
2.50 + self.handle_message_text(text)
2.51
2.52 - else:
2.53 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.54 - request.write("The message could not be decrypted.")
2.55 + # Otherwise, reject the unverified message.
2.56
2.57 - finally:
2.58 - cmd.stdout.close()
2.59 - cmd.stderr.close()
2.60 + except MoinMessageError:
2.61 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.62 + request.write("The message could not be decrypted.")
2.63
2.64 def handle_signed_message(self, message):
2.65
2.66 @@ -163,49 +151,27 @@
2.67 if not homedir:
2.68 return
2.69
2.70 - # Write the detached signature and content to files.
2.71 + gpg = GPG(homedir)
2.72
2.73 - signature_fd, signature_filename = mkstemp()
2.74 - content_fd, content_filename = mkstemp()
2.75 + # Verify the message.
2.76 +
2.77 try:
2.78 - signature_fp = os.fdopen(signature_fd, "w")
2.79 - content_fp = os.fdopen(content_fd, "w")
2.80 - try:
2.81 - signature_fp.write(signature.get_payload())
2.82 - content_fp.write(content.as_string())
2.83 - finally:
2.84 - signature_fp.close()
2.85 - content_fp.close()
2.86 + gpg.verifyMessage(signature.get_payload(), content.as_string())
2.87
2.88 - # Verify the message text.
2.89 + # Log non-fatal errors.
2.90
2.91 - cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename],
2.92 - stderr=PIPE)
2.93 -
2.94 - errors = cmd.stderr.read()
2.95 - if errors:
2.96 - getLogger(__name__).warning(errors)
2.97 + if gpg.errors:
2.98 + getLogger(__name__).warning(gpg.errors)
2.99
2.100 # Handle the embedded message.
2.101
2.102 - try:
2.103 - # With a zero return code, accept the message.
2.104 + self.handle_message_content(content)
2.105
2.106 - if not cmd.wait():
2.107 - self.handle_message_content(content)
2.108 -
2.109 - # Otherwise, reject the unverified message.
2.110 + # Otherwise, reject the unverified message.
2.111
2.112 - else:
2.113 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.114 - request.write("The message could not be verified.")
2.115 -
2.116 - finally:
2.117 - cmd.stderr.close()
2.118 -
2.119 - finally:
2.120 - os.remove(signature_filename)
2.121 - os.remove(content_filename)
2.122 + except MoinMessageError:
2.123 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.124 + request.write("The message could not be verified.")
2.125
2.126 def handle_message_content(self, message):
2.127
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/setup.py Fri Oct 19 00:33:37 2012 +0200
3.3 @@ -0,0 +1,13 @@
3.4 +#! /usr/bin/env python
3.5 +
3.6 +from distutils.core import setup
3.7 +
3.8 +setup(
3.9 + name = "MoinMessage",
3.10 + description = "Send and receive GPG/PGP-signed/encrypted messages using MoinMoin",
3.11 + author = "Paul Boddie",
3.12 + author_email = "paul@boddie.org.uk",
3.13 + url = "http://hgweb.boddie.org.uk/MoinMessage",
3.14 + version = "0.1",
3.15 + py_modules = ["MoinMessage"]
3.16 + )
4.1 --- a/tests/test_send.py Sun Jul 22 02:00:31 2012 +0200
4.2 +++ b/tests/test_send.py Fri Oct 19 00:33:37 2012 +0200
4.3 @@ -14,10 +14,11 @@
4.4 message.add_update([MIMEText("An update to the Wiki.", "moin")])
4.5 message.add_update([MIMEText("Another update to the Wiki.", "moin")])
4.6 email_message = message.get_payload()
4.7 + gpg = GPG()
4.8
4.9 try:
4.10 - signed_message = signMessage(email_message, signer)
4.11 - encrypted_message = encryptMessage(signed_message, recipient)
4.12 + signed_message = gpg.signMessage(email_message, signer)
4.13 + encrypted_message = gpg.encryptMessage(signed_message, recipient)
4.14 print sendMessage(encrypted_message, host, path)
4.15 except MoinMessageError, exc:
4.16 print exc