1.1 --- a/MoinMessage.py Mon Apr 06 17:17:41 2015 +0200
1.2 +++ b/MoinMessage.py Mon Apr 06 17:23:49 2015 +0200
1.3 @@ -2,7 +2,7 @@
1.4 """
1.5 MoinMoin - MoinMessage library
1.6
1.7 - @copyright: 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk>
1.8 + @copyright: 2012, 2013, 2014, 2015 by Paul Boddie <paul@boddie.org.uk>
1.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.10 """
1.11
1.12 @@ -20,6 +20,8 @@
1.13 from tempfile import mkstemp
1.14 from urlparse import urlsplit
1.15 from DateSupport import getDateTimeFromRFC2822
1.16 +from GPGUtils import GPG, GPGError, GPGDecodingError, GPGMissingPart, GPGBadContent, \
1.17 + as_string, is_signed, is_encrypted, getContentAndSignature
1.18 import httplib
1.19 import os
1.20
1.21 @@ -139,351 +141,10 @@
1.22
1.23 return message
1.24
1.25 -class MoinMessageError(Exception):
1.26 - pass
1.27 -
1.28 -class MoinMessageDecodingError(Exception):
1.29 - pass
1.30 -
1.31 -class MoinMessageMissingPart(MoinMessageDecodingError):
1.32 - pass
1.33 -
1.34 -class MoinMessageBadContent(MoinMessageDecodingError):
1.35 - pass
1.36 -
1.37 -class MoinMessageTransferError(MoinMessageError):
1.38 - def __init__(self, code, message, body):
1.39 - MoinMessageError.__init__(self, message)
1.40 - self.code = code
1.41 - self.body = body
1.42 -
1.43 -class GPG:
1.44 -
1.45 - "A wrapper around the gpg command using a particular configuration."
1.46 -
1.47 - def __init__(self, homedir=None):
1.48 - self.conf_args = []
1.49 -
1.50 - if homedir:
1.51 - self.conf_args += ["--homedir", homedir]
1.52 -
1.53 - self.errors = None
1.54 -
1.55 - def run(self, args, text=None):
1.56 -
1.57 - """
1.58 - Invoke gpg with the given 'args', supplying the given 'text' to the
1.59 - command directly or, if 'text' is omitted, using a file provided as part
1.60 - of the 'args' if appropriate.
1.61 -
1.62 - Failure to complete the operation will result in a MoinMessageError
1.63 - being raised.
1.64 - """
1.65 -
1.66 - cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE)
1.67 -
1.68 - # Attempt to write input to the command and to read output from the
1.69 - # command.
1.70 -
1.71 - text, self.errors = cmd.communicate(text)
1.72 -
1.73 - # Test for a zero result.
1.74 -
1.75 - if not cmd.returncode:
1.76 - return text
1.77 - else:
1.78 - raise MoinMessageError, self.errors
1.79 -
1.80 - def verifyMessageText(self, signature, content):
1.81 -
1.82 - "Using the given 'signature', verify the given message 'content'."
1.83 -
1.84 - # Write the detached signature and content to files.
1.85 -
1.86 - signature_fd, signature_filename = mkstemp()
1.87 - content_fd, content_filename = mkstemp()
1.88 -
1.89 - try:
1.90 - signature_fp = os.fdopen(signature_fd, "w")
1.91 - content_fp = os.fdopen(content_fd, "w")
1.92 - try:
1.93 - signature_fp.write(signature)
1.94 - content_fp.write(content)
1.95 - finally:
1.96 - signature_fp.close()
1.97 - content_fp.close()
1.98 -
1.99 - # Verify the message text.
1.100 -
1.101 - text = self.run(["--status-fd", "1", "--verify", signature_filename, content_filename])
1.102 -
1.103 - # Return the details of the signing key.
1.104 -
1.105 - identity = None
1.106 - fingerprint = None
1.107 -
1.108 - for line in text.split("\n"):
1.109 - try:
1.110 - prefix, msgtype, digest, details = line.strip().split(" ", 3)
1.111 - except ValueError:
1.112 - continue
1.113 -
1.114 - # Return the fingerprint and identity details.
1.115 -
1.116 - if msgtype == "GOODSIG":
1.117 - identity = details
1.118 - elif msgtype == "VALIDSIG":
1.119 - fingerprint = digest
1.120 -
1.121 - if identity and fingerprint:
1.122 - return fingerprint, identity
1.123 -
1.124 - return None
1.125 -
1.126 - finally:
1.127 - os.remove(signature_filename)
1.128 - os.remove(content_filename)
1.129 -
1.130 - def verifyMessage(self, message):
1.131 -
1.132 - """
1.133 - Verify the given RFC 3156 'message', returning a tuple of the form
1.134 - (fingerprint, identity, content).
1.135 - """
1.136 -
1.137 - content, signature = getContentAndSignature(message)
1.138 -
1.139 - # Verify the message format.
1.140 -
1.141 - if signature.get_content_type() != "application/pgp-signature":
1.142 - raise MoinMessageBadContent
1.143 -
1.144 - # Verify the message.
1.145 -
1.146 - fingerprint, identity = self.verifyMessageText(signature.get_payload(decode=True), as_string(content))
1.147 - return fingerprint, identity, content
1.148 -
1.149 - def signMessage(self, message, keyid):
1.150 -
1.151 - """
1.152 - Return a signed version of 'message' using the given 'keyid'.
1.153 - """
1.154 -
1.155 - # Sign the container's representation.
1.156 -
1.157 - signature = self.run(["--armor", "-u", keyid, "--detach-sig"], as_string(message))
1.158 -
1.159 - # Make the container for the message.
1.160 -
1.161 - signed_message = MIMEMultipart("signed", protocol="application/pgp-signature")
1.162 - signed_message.attach(message)
1.163 -
1.164 - signature_part = MIMEBase("application", "pgp-signature")
1.165 - signature_part.set_payload(signature)
1.166 - signed_message.attach(signature_part)
1.167 -
1.168 - return signed_message
1.169 -
1.170 - def decryptMessageText(self, message):
1.171 -
1.172 - "Return a decrypted version of 'message'."
1.173 -
1.174 - return self.run(["--decrypt"], message)
1.175 -
1.176 - def decryptMessage(self, message):
1.177 -
1.178 - """
1.179 - Decrypt the given RFC 3156 'message', returning the message text.
1.180 - """
1.181 -
1.182 - try:
1.183 - declaration, content = message.get_payload()
1.184 - except ValueError:
1.185 - raise MoinMessageMissingPart
1.186 -
1.187 - # Verify the message format.
1.188 -
1.189 - if content.get_content_type() != "application/octet-stream":
1.190 - raise MoinMessageBadContent
1.191 -
1.192 - # Return the decrypted message text.
1.193 -
1.194 - return self.decryptMessageText(content.get_payload(decode=True))
1.195 -
1.196 - def encryptMessage(self, message, keyid):
1.197 -
1.198 - """
1.199 - Return an encrypted version of 'message' using the given 'keyid'.
1.200 - """
1.201 -
1.202 - text = as_string(message)
1.203 - encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text)
1.204 -
1.205 - # Make the container for the message.
1.206 -
1.207 - encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
1.208 -
1.209 - # For encrypted content, add the declaration and content.
1.210 -
1.211 - declaration = MIMEBase("application", "pgp-encrypted")
1.212 - declaration.set_payload("Version: 1")
1.213 - encrypted_message.attach(declaration)
1.214 -
1.215 - content = MIMEApplication(encrypted, "octet-stream", encode_noop)
1.216 - encrypted_message.attach(content)
1.217 -
1.218 - return encrypted_message
1.219 -
1.220 - def importKeys(self, text):
1.221 -
1.222 - """
1.223 - Import the keys provided by the given 'text'.
1.224 - """
1.225 -
1.226 - self.run(["--import"], text)
1.227 -
1.228 - def exportKey(self, keyid):
1.229 -
1.230 - """
1.231 - Return the "armoured" public key text for 'keyid' as a message part with
1.232 - a suitable media type.
1.233 - See: https://tools.ietf.org/html/rfc3156#section-7
1.234 - """
1.235 -
1.236 - text = self.run(["--armor", "--export", keyid])
1.237 - return MIMEApplication(text, "pgp-keys", encode_noop)
1.238 -
1.239 - def listKeys(self, keyid=None):
1.240 -
1.241 - """
1.242 - Return a list of key details for keys on the keychain, selecting only
1.243 - one specific key if 'keyid' is specified.
1.244 - """
1.245 -
1.246 - text = self.run(["--list-keys", "--with-colons", "--with-fingerprint"] +
1.247 - (keyid and ["0x%s" % keyid] or []))
1.248 - return self._getKeysFromResult(text)
1.249 -
1.250 - def listSignatures(self, keyid=None):
1.251 -
1.252 - """
1.253 - Return a list of key and signature details for keys on the keychain,
1.254 - selecting only one specific key if 'keyid' is specified.
1.255 - """
1.256 -
1.257 - text = self.run(["--list-sigs", "--with-colons", "--with-fingerprint"] +
1.258 - (keyid and ["0x%s" % keyid] or []))
1.259 - return self._getKeysFromResult(text)
1.260 -
1.261 - def getKeysFromMessagePart(self, part):
1.262 -
1.263 - """
1.264 - Process an application/pgp-keys message 'part', returning a list of
1.265 - key details.
1.266 - """
1.267 -
1.268 - return self.getKeysFromString(part.get_payload(decode=True))
1.269 -
1.270 - def getKeysFromString(self, s):
1.271 -
1.272 - """
1.273 - Return a list of key details extracted from the given key block string
1.274 - 's'. Signature information is also included through the use of the gpg
1.275 - verbose option.
1.276 - """
1.277 -
1.278 - text = self.run(["--with-colons", "--with-fingerprint", "-v"], s)
1.279 - return self._getKeysFromResult(text)
1.280 -
1.281 - def _getKeysFromResult(self, text):
1.282 -
1.283 - """
1.284 - Return a list of key details extracted from the given command result
1.285 - 'text'.
1.286 - """
1.287 -
1.288 - keys = []
1.289 - for line in text.split("\n"):
1.290 - try:
1.291 - recordtype, trust, keylength, algorithm, keyid, cdate, expdate, serial, ownertrust, _rest = line.split(":", 9)
1.292 - except ValueError:
1.293 - continue
1.294 -
1.295 - if recordtype == "pub":
1.296 - userid, _rest = _rest.split(":", 1)
1.297 - keys.append({
1.298 - "type" : recordtype, "trust" : trust, "keylength" : keylength,
1.299 - "algorithm" : algorithm, "keyid" : keyid, "cdate" : cdate,
1.300 - "expdate" : expdate, "userid" : userid, "ownertrust" : ownertrust,
1.301 - "fingerprint" : None, "subkeys" : [], "signatures" : []
1.302 - })
1.303 - elif recordtype == "sub" and keys:
1.304 - keys[-1]["subkeys"].append({
1.305 - "trust" : trust, "keylength" : keylength, "algorithm" : algorithm,
1.306 - "keyid" : keyid, "cdate" : cdate, "expdate" : expdate,
1.307 - "ownertrust" : ownertrust
1.308 - })
1.309 - elif recordtype == "fpr" and keys:
1.310 - fingerprint, _rest = _rest.split(":", 1)
1.311 - keys[-1]["fingerprint"] = fingerprint
1.312 - elif recordtype == "sig" and keys:
1.313 - userid, _rest = _rest.split(":", 1)
1.314 - keys[-1]["signatures"].append({
1.315 - "keyid" : keyid, "cdate" : cdate, "expdate" : expdate,
1.316 - "userid" : userid
1.317 - })
1.318 -
1.319 - return keys
1.320 -
1.321 -# Message serialisation functions, working around email module problems.
1.322 -
1.323 -def as_string(message):
1.324 -
1.325 - """
1.326 - Return the string representation of 'message', attempting to preserve the
1.327 - precise original formatting.
1.328 - """
1.329 -
1.330 - out = StringIO()
1.331 - generator = Generator(out, False, 0) # disable reformatting measures
1.332 - generator.flatten(message)
1.333 - return out.getvalue()
1.334 -
1.335 -# Message decoding functions.
1.336 -
1.337 -# Detect PGP/GPG-encoded payloads.
1.338 -# See: http://tools.ietf.org/html/rfc3156
1.339 -
1.340 -def is_signed(message):
1.341 - mimetype = message.get_content_type()
1.342 - encoding = message.get_content_charset()
1.343 -
1.344 - return mimetype == "multipart/signed" and \
1.345 - message.get_param("protocol") == "application/pgp-signature"
1.346 -
1.347 -def is_encrypted(message):
1.348 - mimetype = message.get_content_type()
1.349 - encoding = message.get_content_charset()
1.350 -
1.351 - return mimetype == "multipart/encrypted" and \
1.352 - message.get_param("protocol") == "application/pgp-encrypted"
1.353 -
1.354 -def getContentAndSignature(message):
1.355 -
1.356 - """
1.357 - Return the content and signature parts of the given RFC 3156 'message'.
1.358 -
1.359 - NOTE: RFC 3156 states that signed messages should employ a detached
1.360 - NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
1.361 - NOTE: instead of "BEGIN PGP SIGNATURE".
1.362 - NOTE: The "micalg" parameter is currently not supported.
1.363 - """
1.364 -
1.365 - try:
1.366 - content, signature = message.get_payload()
1.367 - return content, signature
1.368 - except ValueError:
1.369 - raise MoinMessageMissingPart
1.370 +MoinMessageError = GPGError
1.371 +MoinMessageDecodingError = GPGDecodingError
1.372 +MoinMessageMissingPart = GPGMissingPart
1.373 +MoinMessageBadContent = GPGBadContent
1.374
1.375 # Communications functions.
1.376