From fd85cb8f5c260e624b555e2460ee28ed55fd1a5d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 05:52:29 +0000 Subject: [PATCH 001/145] Remove commented out _init_decorator() from GPGMeta class. --- src/gnupg.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index b2375f3..2b1c647 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -147,19 +147,6 @@ class GPGMeta(type): setattr(cls, '_agent_proc', proc) return True - ## xxx we might not need this, try setting: - ## attrs['remove_path'] = __remove_path__ - - # @classmethod - # def _init_decorator(cls): - # """Wraps the :meth:__init__ function in a partial of itself.""" - # log.debug("_init_decorator called for %s" % cls.__init__.__repr__()) - # def _init_wrapper(*args, **kwargs): - # wraps(cls.__init__, *args, **kwargs) - # if getattr(cls, '_agent_proc', None) is not None: - # cls.__remove_path__(prog='pinentry') - # return _init_wrapper - class GPGBase(object): """Base class to control process initialisation and for property storage.""" From 1d2257db57fc78938fab8ddb0858399548b18380 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 06:13:56 +0000 Subject: [PATCH 002/145] Ignore the quilt applied patches directory. --- .gitignore | 13 +- .pc/.quilt_patches | 1 - .pc/.quilt_series | 1 - .pc/.version | 1 - .../.timestamp | 0 .../gnupg.py | 1701 ----------------- .pc/applied-patches | 1 - 7 files changed, 7 insertions(+), 1711 deletions(-) delete mode 100644 .pc/.quilt_patches delete mode 100644 .pc/.quilt_series delete mode 100644 .pc/.version delete mode 100644 .pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/.timestamp delete mode 100644 .pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/gnupg.py delete mode 100644 .pc/applied-patches diff --git a/.gitignore b/.gitignore index 1b0ee02..0992271 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,11 @@ tags *.org # Ignore log files and directories from tests: -keys/* +*keys/* +*generated-keys* +*.pubring +*.secring +*random_seed* *.log # Ignore distutils record of installed files: @@ -67,8 +71,5 @@ local/* # Ignore gpg binary symlinks: gpg -# Ignore keys generated during tests: -*generated-keys* -*.pubring -*.secring -*random_seed* +# Ignore quilt patch directory: +.pc/* diff --git a/.pc/.quilt_patches b/.pc/.quilt_patches deleted file mode 100644 index 4baccb8..0000000 --- a/.pc/.quilt_patches +++ /dev/null @@ -1 +0,0 @@ -patches diff --git a/.pc/.quilt_series b/.pc/.quilt_series deleted file mode 100644 index c206706..0000000 --- a/.pc/.quilt_series +++ /dev/null @@ -1 +0,0 @@ -series diff --git a/.pc/.version b/.pc/.version deleted file mode 100644 index 0cfbf08..0000000 --- a/.pc/.version +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/.pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/.timestamp b/.pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/.timestamp deleted file mode 100644 index e69de29..0000000 diff --git a/.pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/gnupg.py b/.pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/gnupg.py deleted file mode 100644 index 4c62e0c..0000000 --- a/.pc/0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch/gnupg.py +++ /dev/null @@ -1,1701 +0,0 @@ -#!/usr/bin/env python -#-*- encoding: utf-8 -*- -""" -gnupg.py -======== -A Python interface to GnuPG. - -This is a modified version of python-gnupg-0.3.0, which was created by Vinay -Sajip, which itself is a modification of GPG.py written by Steve Traugott, -which in turn is a modification of the pycrypto GnuPG interface written by -A.M. Kuchling. - -This version is patched to exclude calls to :class:`subprocess.Popen([...], -shell=True)`, and it also attempts to provide sanitization of arguments -presented to gnupg, in order to avoid potential vulnerabilities. - -@authors: A.M. Kuchling - Steve Traugott - Vinay Sajip - Isis Lovecruft, 0x2cdb8b35 - -Steve Traugott's documentation: -------------------------------- - Portions of this module are derived from A.M. Kuchling's well-designed - GPG.py, using Richard Jones' updated version 1.3, which can be found in - the pycrypto CVS repository on Sourceforge: - - http://pycrypto.cvs.sourceforge.net/viewvc/pycrypto/gpg/GPG.py - - This module is *not* forward-compatible with amk's; some of the old - interface has changed. For instance, since I've added decrypt - functionality, I elected to initialize with a 'gpghome' argument instead - of 'keyring', so that gpg can find both the public and secret keyrings. - I've also altered some of the returned objects in order for the caller to - not have to know as much about the internals of the result classes. - - While the rest of ISconf is released under the GPL, I am releasing this - single file under the same terms that A.M. Kuchling used for pycrypto. - - Steve Traugott, stevegt@terraluna.org - Thu Jun 23 21:27:20 PDT 2005 - -Vinay Sajip's documentation: ----------------------------- - This version of the module has been modified from Steve Traugott's version - (see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by - Vinay Sajip to make use of the subprocess module (Steve's version uses - os.fork() and so does not work on Windows). Renamed to gnupg.py to avoid - confusion with the previous versions. - - A unittest harness (test_gnupg.py) has also been added. - - Modifications Copyright (C) 2008-2012 Vinay Sajip. All rights reserved. -""" - -__version__ = "0.3.1" -__author__ = "Isis Agora Lovecruft" -__date__ = "12 Febuary 2013" - -import locale - -try: - from io import StringIO -except ImportError: - from cStringIO import StringIO - -import codecs -import locale -import logging -import os -import re -import socket -from subprocess import Popen -from subprocess import PIPE -import sys -import tempfile -import threading - -try: - import logging.NullHandler as NullHandler -except ImportError: - class NullHandler(logging.Handler): - def handle(self, record): - pass -try: - unicode - _py3k = False -except NameError: - _py3k = True - -logger = logging.getLogger(__name__) -if not logger.handlers: - logger.addHandler(NullHandler()) - -def _copy_data(instream, outstream): - """ - Copy data from one stream to another. - - @param instream: A file descriptor to read from. - @param outstream: A file descriptor to write to. - """ - sent = 0 - - try: - assert isinstance(instream, file), "instream is not a file" - assert isinstance(outsteam, file), "outstream is not a file" - except AssertionError as ae: - logger.exception(ae) - return - - if hasattr(sys.stdin, 'encoding'): - enc = sys.stdin.encoding - else: - enc = 'ascii' - - while True: - data = instream.read(1024) - if len(data) == 0: - break - sent += len(data) - logger.debug("sending chunk (%d): %r", sent, data[:256]) - try: - outstream.write(data) - except UnicodeError: - try: - outstream.write(data.encode(enc)) - except IOError: - logger.exception('Error sending data: Broken pipe') - break - except IOError: - # Can sometimes get 'broken pipe' errors even when the - # data has all been sent - logger.exception('Error sending data: Broken pipe') - break - try: - outstream.close() - except IOError: - logger.exception('Got IOError while trying to close FD outstream') - else: - logger.debug("closed output, %d bytes sent", sent) - -def _threaded_copy_data(instream, outstream): - wr = threading.Thread(target=_copy_data, args=(instream, outstream)) - wr.setDaemon(True) - logger.debug('data copier: %r, %r, %r', wr, instream, outstream) - wr.start() - return wr - -def _write_passphrase(stream, passphrase, encoding): - passphrase = '%s\n' % passphrase - passphrase = passphrase.encode(encoding) - stream.write(passphrase) - logger.debug("Wrote passphrase.") - -def _is_sequence(instance): - return isinstance(instance,list) or isinstance(instance,tuple) - -def _make_binary_stream(s, encoding): - try: - if _py3k: - if isinstance(s, str): - s = s.encode(encoding) - else: - if type(s) is not str: - s = s.encode(encoding) - from io import BytesIO - rv = BytesIO(s) - except ImportError: - rv = StringIO(s) - return rv - -class Verify(object): - "Handle status messages for --verify" - - TRUST_UNDEFINED = 0 - TRUST_NEVER = 1 - TRUST_MARGINAL = 2 - TRUST_FULLY = 3 - TRUST_ULTIMATE = 4 - - TRUST_LEVELS = { - "TRUST_UNDEFINED" : TRUST_UNDEFINED, - "TRUST_NEVER" : TRUST_NEVER, - "TRUST_MARGINAL" : TRUST_MARGINAL, - "TRUST_FULLY" : TRUST_FULLY, - "TRUST_ULTIMATE" : TRUST_ULTIMATE, - } - - def __init__(self, gpg): - self.gpg = gpg - self.valid = False - self.fingerprint = self.creation_date = self.timestamp = None - self.signature_id = self.key_id = None - self.username = None - self.status = None - self.pubkey_fingerprint = None - self.expire_timestamp = None - self.sig_timestamp = None - self.trust_text = None - self.trust_level = None - - def __nonzero__(self): - return self.valid - - __bool__ = __nonzero__ - - def handle_status(self, key, value): - if key in self.TRUST_LEVELS: - self.trust_text = key - self.trust_level = self.TRUST_LEVELS[key] - elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", - "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", - "DECRYPTION_OKAY", "INV_SGNR"): - pass - elif key == "BADSIG": - self.valid = False - self.status = 'signature bad' - self.key_id, self.username = value.split(None, 1) - elif key == "GOODSIG": - self.valid = True - self.status = 'signature good' - self.key_id, self.username = value.split(None, 1) - elif key == "VALIDSIG": - (self.fingerprint, - self.creation_date, - self.sig_timestamp, - self.expire_timestamp) = value.split()[:4] - # may be different if signature is made with a subkey - self.pubkey_fingerprint = value.split()[-1] - self.status = 'signature valid' - elif key == "SIG_ID": - (self.signature_id, - self.creation_date, self.timestamp) = value.split() - elif key == "ERRSIG": - self.valid = False - (self.key_id, - algo, hash_algo, - cls, - self.timestamp) = value.split()[:5] - self.status = 'signature error' - elif key == "DECRYPTION_FAILED": - self.valid = False - self.key_id = value - self.status = 'decryption failed' - elif key == "NO_PUBKEY": - self.valid = False - self.key_id = value - self.status = 'no public key' - elif key in ("KEYEXPIRED", "SIGEXPIRED"): - # these are useless in verify, since they are spit out for any - # pub/subkeys on the key, not just the one doing the signing. - # if we want to check for signatures with expired key, - # the relevant flag is EXPKEYSIG. - pass - elif key in ("EXPKEYSIG", "REVKEYSIG"): - # signed with expired or revoked key - self.valid = False - self.key_id = value.split()[0] - self.status = (('%s %s') % (key[:3], key[3:])).lower() - else: - raise ValueError("Unknown status message: %r" % key) - -class ImportResult(object): - "Handle status messages for --import" - - counts = '''count no_user_id imported imported_rsa unchanged - n_uids n_subk n_sigs n_revoc sec_read sec_imported - sec_dups not_imported'''.split() - def __init__(self, gpg): - self.gpg = gpg - self.imported = [] - self.results = [] - self.fingerprints = [] - for result in self.counts: - setattr(self, result, None) - - def __nonzero__(self): - if self.not_imported: return False - if not self.fingerprints: return False - return True - - __bool__ = __nonzero__ - - ok_reason = { - '0': 'Not actually changed', - '1': 'Entirely new key', - '2': 'New user IDs', - '4': 'New signatures', - '8': 'New subkeys', - '16': 'Contains private key', - } - - problem_reason = { - '0': 'No specific reason given', - '1': 'Invalid Certificate', - '2': 'Issuer Certificate missing', - '3': 'Certificate Chain too long', - '4': 'Error storing certificate', - } - - def handle_status(self, key, value): - if key == "IMPORTED": - # this duplicates info we already see in import_ok & import_problem - pass - elif key == "NODATA": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'No valid data found'}) - elif key == "IMPORT_OK": - reason, fingerprint = value.split() - reasons = [] - for code, text in list(self.ok_reason.items()): - if int(reason) | int(code) == int(reason): - reasons.append(text) - reasontext = '\n'.join(reasons) + "\n" - self.results.append({'fingerprint': fingerprint, - 'ok': reason, 'text': reasontext}) - self.fingerprints.append(fingerprint) - elif key == "IMPORT_PROBLEM": - try: - reason, fingerprint = value.split() - except: - reason = value - fingerprint = '' - self.results.append({'fingerprint': fingerprint, - 'problem': reason, 'text': self.problem_reason[reason]}) - elif key == "IMPORT_RES": - import_res = value.split() - for i in range(len(self.counts)): - setattr(self, self.counts[i], int(import_res[i])) - elif key == "KEYEXPIRED": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Key expired'}) - elif key == "SIGEXPIRED": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Signature expired'}) - else: - raise ValueError("Unknown status message: %r" % key) - - def summary(self): - l = [] - l.append('%d imported' % self.imported) - if self.not_imported: - l.append('%d not imported' % self.not_imported) - return ', '.join(l) - -ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) - -class ListKeys(list): - ''' Handle status messages for --list-keys. - - Handle pub and uid (relating the latter to the former). - - Don't care about (info from src/DETAILS): - - crt = X.509 certificate - crs = X.509 certificate and private key available - ssb = secret subkey (secondary key) - uat = user attribute (same as user id except for field 10). - sig = signature - rev = revocation signature - pkd = public key data (special field format, see below) - grp = reserved for gpgsm - rvk = revocation key - ''' - def __init__(self, gpg): - self.gpg = gpg - self.curkey = None - self.fingerprints = [] - self.uids = [] - - def key(self, args): - vars = (""" - type trust length algo keyid date expires dummy ownertrust uid - """).split() - self.curkey = {} - for i in range(len(vars)): - self.curkey[vars[i]] = args[i] - self.curkey['uids'] = [] - if self.curkey['uid']: - self.curkey['uids'].append(self.curkey['uid']) - del self.curkey['uid'] - self.curkey['subkeys'] = [] - self.append(self.curkey) - - pub = sec = key - - def fpr(self, args): - self.curkey['fingerprint'] = args[9] - self.fingerprints.append(args[9]) - - def uid(self, args): - uid = args[9] - uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) - self.curkey['uids'].append(uid) - self.uids.append(uid) - - def sub(self, args): - subkey = [args[4], args[11]] - self.curkey['subkeys'].append(subkey) - - def handle_status(self, key, value): - pass - -class Crypt(Verify): - "Handle status messages for --encrypt and --decrypt" - def __init__(self, gpg): - Verify.__init__(self, gpg) - self.data = '' - self.ok = False - self.status = '' - - def __nonzero__(self): - if self.ok: return True - return False - - __bool__ = __nonzero__ - - def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) - - def handle_status(self, key, value): - if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", - "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", - "CARDCTRL"): - # in the case of ERROR, this is because a more specific error - # message will have come first - pass - elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", - "MISSING_PASSPHRASE", "DECRYPTION_FAILED", - "KEY_NOT_CREATED"): - self.status = key.replace("_", " ").lower() - elif key == "NEED_PASSPHRASE_SYM": - self.status = 'need symmetric passphrase' - elif key == "BEGIN_DECRYPTION": - self.status = 'decryption incomplete' - elif key == "BEGIN_ENCRYPTION": - self.status = 'encryption incomplete' - elif key == "DECRYPTION_OKAY": - self.status = 'decryption ok' - self.ok = True - elif key == "END_ENCRYPTION": - self.status = 'encryption ok' - self.ok = True - elif key == "INV_RECP": - self.status = 'invalid recipient' - elif key == "KEYEXPIRED": - self.status = 'key expired' - elif key == "SIG_CREATED": - self.status = 'sig created' - elif key == "SIGEXPIRED": - self.status = 'sig expired' - else: - Verify.handle_status(self, key, value) - -class GenKey(object): - "Handle status messages for --gen-key" - def __init__(self, gpg): - self.gpg = gpg - self.type = None - self.fingerprint = None - - def __nonzero__(self): - if self.fingerprint: return True - return False - - __bool__ = __nonzero__ - - def __str__(self): - return self.fingerprint or '' - - def handle_status(self, key, value): - if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA", "KEY_NOT_CREATED"): - pass - elif key == "KEY_CREATED": - (self.type, self.fingerprint) = value.split() - else: - raise ValueError("Unknown status message: %r" % key) - -class DeleteResult(object): - "Handle status messages for --delete-key and --delete-secret-key" - def __init__(self, gpg): - self.gpg = gpg - self.status = 'ok' - - def __str__(self): - return self.status - - problem_reason = { - '1': 'No such key', - '2': 'Must delete secret key first', - '3': 'Ambigious specification', - } - - def handle_status(self, key, value): - if key == "DELETE_PROBLEM": - self.status = self.problem_reason.get(value, "Unknown error: %r" - % value) - else: - raise ValueError("Unknown status message: %r" % key) - -class Sign(object): - "Handle status messages for --sign" - def __init__(self, gpg): - self.gpg = gpg - self.type = None - self.fingerprint = None - - def __nonzero__(self): - return self.fingerprint is not None - - __bool__ = __nonzero__ - - def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) - - def handle_status(self, key, value): - if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", - "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", "INV_SGNR"): - pass - elif key == "SIG_CREATED": - (self.type, algo, hashalgo, cls, self.timestamp, - self.fingerprint) = value.split() - else: - raise ValueError("Unknown status message: %r" % key) - -class ProtectedOption(Exception): - """Raised when the option passed to GPG is disallowed.""" - -class UsageError(Exception): - """Raised when you're Doing It Wrong.""" - -def _fix_unsafe(input): - """ - Find characters used to escape from a string into a shell, and wrap them - in quotes if they exist. Regex pilfered from python-3.x shlex module. - - @param input: The input intended for the gnupg process. - """ - ## xxx do we want to add ';'? - _unsafe = re.compile(r'[^\w@%+=:,./-]', 256) - try: - if len(_unsafe.findall(input)) == 0: - return input - else: - clean = "'" + input.replace("'", "'\"'\"'") + "'" - return clean - except TypeError: - return None - -def _is_file(input): - """ - Check that the size of the thing which is supposed to be a filename has - size greater than zero, without following symbolic links or using - :func:`os.path.isfile`. - """ - try: - assert os.lstat(input).st_size > 0, "not a file: %s" % input - except (AssertionError, TypeError) as error: - logger.debug(error.message) - return False - else: - return True - -def _has_readwrite(path): - """ - Determine if the real uid/gid of the executing user has read and write - permissions for a directory or a file. - - @type path: C{str} - @param path: The path to the directory or file to check permissions for. - - @rtype: C{bool} - @param: True if real uid/gid has read+write permissions, False otherwise. - """ - return os.access(path, os.R_OK and os.W_OK) - -def _underscore(input, remove_prefix=False): - """ - Change hyphens to underscores so that GPG option names can be easily - tranlated to object attributes. - - @type input: C{str} - @param input: The input intended for the gnupg process. - - @type remove_prefix: C{bool} - @param remove_prefix: If True, strip leading hyphens from the input. - - @rtype: C{str} - @return: The :param:input with hyphens changed to underscores. - """ - if not remove_prefix: - return input.replace('-', '_') - else: - return input.lstrip('-').replace('-', '_') - -def _hyphenate(input, add_prefix=False): - """ - Change underscores to hyphens so that object attributes can be easily - tranlated to GPG option names. - - @type input: C{str} - @param input: The attribute to hyphenate. - - @type add_prefix: C{bool} - @param add_prefix: If True, add leading hyphens to the input. - - @rtype: C{str} - @return: The :param:input with underscores changed to hyphens. - """ - ret = '--' if add_prefix else '' - ret += input.replace('_', '-') - return ret - -def _is_allowed(input): - """ - Check that an option or argument given to GPG is in the set of allowed - options, the latter being a strict subset of the set of all options known - to GPG. - - @type input: C{str} - @param input: An input meant to be parsed as an option or flag to the GnuPG - process. Should be formatted the same as an option or flag - to the commandline gpg, i.e. "--encrypt-files". - - @type _possible: C{frozenset} - @ivar _possible: All known GPG options and flags. - - @type _allowed: C{frozenset} - @ivar _allowed: All allowed GPG options and flags, e.g. all GPG options and - flags which we are willing to acknowledge and parse. If we - want to support a new option, it will need to have its own - parsing class and its name will need to be added to this - set. - - @rtype: C{Exception} or C{str} - @raise: UsageError if :ivar:_allowed is not a subset of :ivar:_possible. - ProtectedOption if :param:input is not in the set :ivar:_allowed. - @return: The original parameter :param:input, unmodified and unsanitized, - if no errors occur. - """ - - _all = (""" ---allow-freeform-uid --multifile ---allow-multiple-messages --no ---allow-multisig-verification --no-allow-freeform-uid ---allow-non-selfsigned-uid --no-allow-multiple-messages ---allow-secret-key-import --no-allow-non-selfsigned-uid ---always-trust --no-armor ---armor --no-armour ---armour --no-ask-cert-expire ---ask-cert-expire --no-ask-cert-level ---ask-cert-level --no-ask-sig-expire ---ask-sig-expire --no-auto-check-trustdb ---attribute-fd --no-auto-key-locate ---attribute-file --no-auto-key-retrieve ---auto-check-trustdb --no-batch ---auto-key-locate --no-comments ---auto-key-retrieve --no-default-keyring ---batch --no-default-recipient ---bzip2-compress-level --no-disable-mdc ---bzip2-decompress-lowmem --no-emit-version ---card-edit --no-encrypt-to ---card-status --no-escape-from-lines ---cert-digest-algo --no-expensive-trust-checks ---cert-notation --no-expert ---cert-policy-url --no-force-mdc ---change-pin --no-force-v3-sigs ---charset --no-force-v4-certs ---check-sig --no-for-your-eyes-only ---check-sigs --no-greeting ---check-trustdb --no-groups ---cipher-algo --no-literal ---clearsign --no-mangle-dos-filenames ---command-fd --no-mdc-warning ---command-file --no-options ---comment --no-permission-warning ---completes-needed --no-pgp2 ---compress-algo --no-pgp6 ---compression-algo --no-pgp7 ---compress-keys --no-pgp8 ---compress-level --no-random-seed-file ---compress-sigs --no-require-backsigs ---ctapi-driver --no-require-cross-certification ---dearmor --no-require-secmem ---dearmour --no-rfc2440-text ---debug --no-secmem-warning ---debug-all --no-show-notation ---debug-ccid-driver --no-show-photos ---debug-level --no-show-policy-url ---decrypt --no-sig-cache ---decrypt-files --no-sig-create-check ---default-cert-check-level --no-sk-comments ---default-cert-expire --no-strict ---default-cert-level --notation-data ---default-comment --not-dash-escaped ---default-key --no-textmode ---default-keyserver-url --no-throw-keyid ---default-preference-list --no-throw-keyids ---default-recipient --no-tty ---default-recipient-self --no-use-agent ---default-sig-expire --no-use-embedded-filename ---delete-keys --no-utf8-strings ---delete-secret-and-public-keys --no-verbose ---delete-secret-keys --no-version ---desig-revoke --openpgp ---detach-sign --options ---digest-algo --output ---disable-ccid --override-session-key ---disable-cipher-algo --passphrase ---disable-dsa2 --passphrase-fd ---disable-mdc --passphrase-file ---disable-pubkey-algo --passphrase-repeat ---display --pcsc-driver ---display-charset --personal-cipher-preferences ---dry-run --personal-cipher-prefs ---dump-options --personal-compress-preferences ---edit-key --personal-compress-prefs ---emit-version --personal-digest-preferences ---enable-dsa2 --personal-digest-prefs ---enable-progress-filter --pgp2 ---enable-special-filenames --pgp6 ---enarmor --pgp7 ---enarmour --pgp8 ---encrypt --photo-viewer ---encrypt-files --pipemode ---encrypt-to --preserve-permissions ---escape-from-lines --primary-keyring ---exec-path --print-md ---exit-on-status-write-error --print-mds ---expert --quick-random ---export --quiet ---export-options --reader-port ---export-ownertrust --rebuild-keydb-caches ---export-secret-keys --recipient ---export-secret-subkeys --recv-keys ---fast-import --refresh-keys ---fast-list-mode --remote-user ---fetch-keys --require-backsigs ---fingerprint --require-cross-certification ---fixed-list-mode --require-secmem ---fix-trustdb --rfc1991 ---force-mdc --rfc2440 ---force-ownertrust --rfc2440-text ---force-v3-sigs --rfc4880 ---force-v4-certs --run-as-shm-coprocess ---for-your-eyes-only --s2k-cipher-algo ---gen-key --s2k-count ---gen-prime --s2k-digest-algo ---gen-random --s2k-mode ---gen-revoke --search-keys ---gnupg --secret-keyring ---gpg-agent-info --send-keys ---gpgconf-list --set-filename ---gpgconf-test --set-filesize ---group --set-notation ---help --set-policy-url ---hidden-encrypt-to --show-keyring ---hidden-recipient --show-notation ---homedir --show-photos ---honor-http-proxy --show-policy-url ---ignore-crc-error --show-session-key ---ignore-mdc-error --sig-keyserver-url ---ignore-time-conflict --sign ---ignore-valid-from --sign-key ---import --sig-notation ---import-options --sign-with ---import-ownertrust --sig-policy-url ---interactive --simple-sk-checksum ---keyid-format --sk-comments ---keyring --skip-verify ---keyserver --status-fd ---keyserver-options --status-file ---lc-ctype --store ---lc-messages --strict ---limit-card-insert-tries --symmetric ---list-config --temp-directory ---list-key --textmode ---list-keys --throw-keyid ---list-only --throw-keyids ---list-options --trustdb-name ---list-ownertrust --trusted-key ---list-packets --trust-model ---list-public-keys --try-all-secrets ---list-secret-keys --ttyname ---list-sig --ttytype ---list-sigs --ungroup ---list-trustdb --update-trustdb ---load-extension --use-agent ---local-user --use-embedded-filename ---lock-multiple --user ---lock-never --utf8-strings ---lock-once --verbose ---logger-fd --verify ---logger-file --verify-files ---lsign-key --verify-options ---mangle-dos-filenames --version ---marginals-needed --warranty ---max-cert-depth --with-colons ---max-output --with-fingerprint ---merge-only --with-key-data ---min-cert-level --yes -""").split() - - _possible = frozenset(_all) - - ## these are the allowed options we will handle so far, all others should - ## be dropped. this dance is so that when new options are added later, we - ## merely add the to the _allowed list, and the `` _allowed.issubset`` - ## assertion will check that GPG will recognise them - ## - ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] - ## - ## xxx which ones do we want as defaults? - ## eg, --no-show-photos would mitigate things like - ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 - _allowed = frozenset( - ['--list-packets', '--delete-keys', '--delete-secret-keys', - '--encrypt', '--print-mds', '--print-md', '--sign', - '--encrypt-files', '--gen-key', '--decrypt', '--decrypt-files', - '--list-keys', '--import', '--verify', '--version', - '--status-fd', '--no-tty', '--homedir', '--no-default-keyring', - '--keyring', '--passphrase-fd']) - - ## check that _allowed is a subset of _possible - try: - assert _allowed.issubset(_possible), \ - '_allowed is not subset of known options, difference: %s' \ - % _allowed.difference(_possible) - except AssertionError as ae: ## 'as' syntax requires python>=2.6 - logger.debug("gnupg._is_allowed(): %s" % ae.message) - raise UsageError(ae.message) - - ## if we got a list of args, join them - if not isinstance(input, str): - input = ' '.join([x for x in input]) - - if isinstance(input, str): - if input.find('_') > 0: - if not input.startswith('--'): - hyphenated = _hyphenate(input, add_prefix=True) - else: - hyphenated = _hyphenate(input) - else: - hyphenated = input - try: - assert hyphenated in _allowed - except AssertionError as ae: - logger.warn("Dropping option '%s'..." - % _fix_unsafe(hyphenated)) - raise ProtectedOption("Option '%s' not supported." - % _fix_unsafe(hyphenated)) - else: - logger.debug("Got allowed option '%s'." - % _fix_unsafe(hyphenated)) - return input - return None - -def _sanitise(*args): - """ - Take an arg or the key portion of a kwarg and check that it is in the set - of allowed GPG options and flags, and that it has the correct type. Then, - attempt to escape any unsafe characters. If an option is not allowed, - drop it with a logged warning. Returns a dictionary of all sanitised, - allowed options. - - Each new option that we support that is not a boolean, but instead has - some extra inputs, i.e. "--encrypt-file foo.txt", will need some basic - safety checks added here. - - GnuPG has three-hundred and eighteen commandline flags. Also, not all - implementations of OpenPGP parse PGP packets and headers in the same way, - so there is added potential there for messing with calls to GPG. - - For information on the PGP message format specification, see: - https://www.ietf.org/rfc/rfc1991.txt - - If you're asking, "Is this *really* necessary?": No. Not really. See: - https://xkcd.com/1181/ - - @type args: C{str} - @param args: (optional) The boolean arguments which will be passed to the - GnuPG process. - @rtype: C{str} - @param: :ivar:sanitised - """ - - def _check_arg_and_value(arg, value): - """ - Check that a single :param:arg is an allowed option. If it is allowed, - quote out any escape characters in :param:values, and add the pair to - :ivar:sanitised. - - @type arg: C{str} - - @param arg: The arguments which will be passed to the GnuPG process, - and, optionally their corresponding values. The values are - any additional arguments following the GnuPG option or - flag. For example, if we wanted to pass "--encrypt - --recipient isis@leap.se" to gpg, then "--encrypt" would be - an arg without a value, and "--recipient" would also be an - arg, with a value of "isis@leap.se". - @type sanitised: C{str} - @ivar sanitised: The sanitised, allowed options. - """ - safe_values = str() - - try: - allowed_flag = _is_allowed(arg) - except AssertionError as ae: - logger.warn(ae) - logger.warn("Dropping option '%s'..." % _fix_unsafe(arg)) - except ProtectedOption: - logger.warn("Dropping option '%s'..." % _fix_unsafe(arg)) - else: - if allowed_flag is not None: - safe_values += (allowed_flag + " ") - if isinstance(value, str): - value_list = [] - if value.find(' ') > 0: - value_list = value.split(' ') - else: - logger.debug("_check_values(): got non-string for values") - for value in value_list: - safe_value = _fix_unsafe(value) - if allowed_flag == '--encrypt' or '--encrypt-files' \ - or '--decrypt' or '--decrypt-file' \ - or '--import' or '--verify': - ## xxx what other things should we check for? - ## Place checks here: - if _is_file(safe_value): - safe_values += (safe_value + " ") - else: - logger.debug("Got non-filename for %s option: %s" - % (allowed_flag, safe_value)) - else: - safe_values += (safe_value + " ") - logger.debug("Got non-checked value: %s" % safe_value) - else: - logger.debug("Got null allowed_flag.") - return safe_values - - checked = [] - - if args is not None: - for arg in args: - if isinstance(arg, str): - logger.debug("_sanitise(): Got arg string: %s" % arg) - ## if we're given a string with a bunch of options in it split - ## them up and deal with them separately - if arg.find(' ') > 0: - filo = arg.split() - filo.reverse() - is_flag = lambda x: x.startswith('-') - while len(filo) > 0: - new_values = str() - if is_flag(filo[0]) and is_flag(filo[1]): - new_arg = filo.pop() - else: - new_arg = filo.pop() - while not is_flag(filo[0]): - new_values += (filo.pop() + ' ') - safe = _check_arg_and_value(new_arg, new_values) - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - else: - safe = _check_arg_and_value(arg, None) - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - elif isinstance(arg, list): ## happens with '--version' - logger.debug("_sanitise(): Got arg list: %s" % arg) - for a in arg: - if a.startswith('--'): - safe = _check_arg_and_value(a, None) - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - else: - logger.debug("_sanitise(): got non string or list arg: %s" % arg) - - sanitised = ' '.join(x for x in checked) - return sanitised - -def _which(executable, flags=os.X_OK): - """Borrowed from Twisted's :mod:twisted.python.proutils . - - Search PATH for executable files with the given name. - - On newer versions of MS-Windows, the PATHEXT environment variable will be - set to the list of file extensions for files considered executable. This - will normally include things like ".EXE". This fuction will also find files - with the given name ending with any of these extensions. - - On MS-Windows the only flag that has any meaning is os.F_OK. Any other - flags will be ignored. - - Note: This function does not help us prevent an attacker who can already - manipulate the environment's PATH settings from placing malicious code - higher in the PATH. It also does happily follows links. - - @type name: C{str} - @param name: The name for which to search. - @type flags: C{int} - @param flags: Arguments to L{os.access}. - @rtype: C{list} - @param: A list of the full paths to files found, in the order in which - they were found. - """ - result = [] - exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) - path = os.environ.get('PATH', None) - if path is None: - return [] - for p in os.environ.get('PATH', '').split(os.pathsep): - p = os.path.join(p, executable) - if os.access(p, flags): - result.append(p) - for e in exts: - pext = p + e - if os.access(pext, flags): - result.append(pext) - return result - -class GPG(object): - """Encapsulate access to the gpg executable""" - decode_errors = 'strict' - - result_map = {'crypt': Crypt, - 'delete': DeleteResult, - 'generate': GenKey, - 'import': ImportResult, - 'list': ListKeys, - 'sign': Sign, - 'verify': Verify,} - - def __init__(self, gpgbinary=None, gpghome=None, - verbose=False, use_agent=False, - keyring=None, options=None): - """ - Initialize a GnuPG process wrapper. - - @type gpgbinary: C{str} - @param gpgbinary: Name for GnuPG binary executable. If the absolute - path is not given, the evironment variable $PATH is - searched for the executable and checked that the - real uid/gid of the user has sufficient permissions. - @type gpghome: C{str} - @param gpghome: Full pathname to directory containing the public and - private keyrings. Default is whatever GnuPG defaults - to. - @param keyring: Name of alternative keyring file to use. If specified, - the default keyring is not used. - @options: A list of additional options to pass to the GPG binary. - - @rtype: - @raises: RuntimeError with explanation message if there is a problem - invoking gpg. - @returns: - """ - - if not gpgbinary: - try: - standard_binary = _which('gpg')[0] - except IndexError: - raise RuntimeError("gpg is not installed") - - ## find the absolute path, check that it is not a link, and check that - ## we have exec permissions - that = _which(gpgbinary or 'gpg') - full = that[0] if (len(that) > 0) else None - if not full: - gpgbinary = 'gpg' - that = _which(gpgbinary) - full = that[0] if (len(that) > 0) else None - self.gpgbinary = full - - self.options = _sanitise(options) if options else None - - self.gpghome = _fix_unsafe(gpghome) - if self.gpghome: - if not os.path.isdir(self.gpghome): - message = ("Creating gpg home dir: %s" % gpghome) - logger.debug("GPG.__init__(): %s" % message) - os.makedirs(self.gpghome, 0x1C0) - if not os.path.isabs(self.gpghome): - message = ("Got non-abs gpg home dir path: %s" % self.gpghome) - logger.debug("GPG.__init__(): %s" % message) - self.gpghome = os.path.abspath(self.gpghome) - else: - message = ("Unsuitable gpg home dir: %s" % gpghome) - logger.debug("GPG.__init__(): %s" % message) - - safe_keyring = _fix_unsafe(keyring) - if not safe_keyring: - safe_keyring = 'secring.gpg' - self.keyring = os.path.join(self.gpghome, safe_keyring) - - ## xxx TODO: hack the locale module away so we can use this on android - self.encoding = locale.getpreferredencoding() - if self.encoding is None: # This happens on Jython! - self.encoding = sys.stdin.encoding - - try: - assert os.path.isabs(full), "Couldn't get full path to gpg" - assert not os.path.islink(full), "Path to gpg binary is link" - assert os.access(full, os.X_OK), "gpg binary must be executable" - assert self.gpghome is not None, "Got None for self.gpghome" - assert _has_readwrite(self.gpghome), ("Home dir %s needs r+w" - % self.gpghome) - assert self.gpgbinary, "Could not find gpgbinary %s" % full - assert isinstance(verbose, bool), "'verbose' must be boolean" - assert isinstance(use_agent, bool), "'use_agent' must be boolean" - if self.options: - assert isinstance(options, str), ("options not formatted: %s" - % options) - except (AssertionError, AttributeError) as ae: - logger.debug("GPG.__init__(): %s" % ae.message) - raise RuntimeError(ae.message) - else: - self.gpgbinary = full - self.verbose = verbose - self.use_agent = use_agent - - proc = self._open_subprocess(["--version"]) - result = self.result_map['verify'](self) - self._collect_output(proc, result, stdin=proc.stdin) - if proc.returncode != 0: - raise RuntimeError("Error invoking gpg: %s: %s" - % (proc.returncode, result.stderr)) - - if self.keyring: - try: - assert _has_readwrite(self.keyring), ("Need r+w for %s" - % self.keyring) - except AssertionError as ae: - logger.debug(ae.message) - - def make_args(self, args, passphrase=False): - """ - Make a list of command line elements for GPG. The value of ``args`` - will be appended. The ``passphrase`` argument needs to be True if - a passphrase will be sent to GPG, else False. - """ - cmd = [self.gpgbinary, '--status-fd 2 --no-tty'] - if self.gpghome: - cmd.append('--homedir "%s"' % self.gpghome) - if self.keyring: - cmd.append('--no-default-keyring --keyring "%s"' % self.keyring) - if passphrase: - cmd.append('--batch --passphrase-fd 0') - if self.use_agent: - cmd.append('--use-agent') - if self.options: - cmd.extend(self.options) - if args: - if isinstance(args, list): - for arg in args: - safe_arg = _sanitise(arg) - if safe_arg != "": - cmd.append(safe_arg) - logger.debug("make_args(): Using command: %s" % cmd) - return cmd - - def _open_subprocess(self, args=None, passphrase=False): - # Internal method: open a pipe to a GPG subprocess and return - # the file objects for communicating with it. - cmd = ' '.join(self.make_args(args, passphrase)) - if self.verbose: - print(cmd) - logger.debug("%s", cmd) - return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) - - def _read_response(self, stream, result): - # Internal method: reads all the stderr output from GPG, taking notice - # only of lines that begin with the magic [GNUPG:] prefix. - # - # Calls methods on the response object for each valid token found, - # with the arg being the remainder of the status line. - lines = [] - while True: - line = stream.readline() - if len(line) == 0: - break - lines.append(line) - line = line.rstrip() - if self.verbose: - print(line) - logger.debug("%s", line) - if line[0:9] == '[GNUPG:] ': - # Chop off the prefix - line = line[9:] - L = line.split(None, 1) - keyword = L[0] - if len(L) > 1: - value = L[1] - else: - value = "" - result.handle_status(keyword, value) - result.stderr = ''.join(lines) - - def _read_data(self, stream, result): - # Read the contents of the file from GPG's stdout - chunks = [] - while True: - data = stream.read(1024) - if len(data) == 0: - break - logger.debug("chunk: %r" % data[:256]) - chunks.append(data) - if _py3k: - # Join using b'' or '', as appropriate - result.data = type(data)().join(chunks) - else: - result.data = ''.join(chunks) - - def _collect_output(self, process, result, writer=None, stdin=None): - """ - Drain the subprocesses output streams, writing the collected output - to the result. If a writer thread (writing to the subprocess) is given, - make sure it's joined before returning. If a stdin stream is given, - close it before returning. - """ - stderr = codecs.getreader(self.encoding)(process.stderr) - rr = threading.Thread(target=self._read_response, args=(stderr, result)) - rr.setDaemon(True) - logger.debug('stderr reader: %r', rr) - rr.start() - - stdout = process.stdout - dr = threading.Thread(target=self._read_data, args=(stdout, result)) - dr.setDaemon(True) - logger.debug('stdout reader: %r', dr) - dr.start() - - dr.join() - rr.join() - if writer is not None: - writer.join() - process.wait() - if stdin is not None: - try: - stdin.close() - except IOError: - pass - stderr.close() - stdout.close() - - def _handle_io(self, args, file, result, passphrase=None, binary=False): - """ - Handle a call to GPG - pass input data, collect output data. - """ - p = self._open_subprocess(args, passphrase is not None) - if not binary: - stdin = codecs.getwriter(self.encoding)(p.stdin) - else: - stdin = p.stdin - if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) - self._collect_output(p, result, writer, stdin) - return result - - # - # SIGNATURE METHODS - # - def sign(self, message, **kwargs): - """sign message""" - f = _make_binary_stream(message, self.encoding) - result = self.sign_file(f, **kwargs) - f.close() - return result - - def sign_file(self, file, keyid=None, passphrase=None, clearsign=True, - detach=False, binary=False): - """sign file""" - logger.debug("sign_file: %s", file) - if binary: - args = ['-s'] - else: - args = ['-sa'] - - if clearsign: - args.append("--clearsign") - if detach: - logger.debug( - "Cannot use --clearsign and --detach-sign simultaneously.") - logger.debug( - "Using default GPG behaviour: --clearsign only.") - elif detach and not clearsign: - args.append("--detach-sign") - - if keyid: - args.append('--default-key "%s"' % keyid) - - result = self.result_map['sign'](self) - #We could use _handle_io here except for the fact that if the - #passphrase is bad, gpg bails and you can't write the message. - p = self._open_subprocess(args, passphrase is not None) - try: - stdin = p.stdin - if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) - except IOError: - logging.exception("error writing message") - writer = None - self._collect_output(p, result, writer, stdin) - return result - - def verify(self, data): - """Verify the signature on the contents of the string 'data' - - >>> gpg = GPG(gpghome="keys") - >>> input = gpg.gen_key_input(Passphrase='foo') - >>> key = gpg.gen_key(input) - >>> assert key - >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='bar') - >>> assert not sig - >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='foo') - >>> assert sig - >>> verify = gpg.verify(sig.data) - >>> assert verify - - """ - f = _make_binary_stream(data, self.encoding) - result = self.verify_file(f) - f.close() - return result - - def verify_file(self, file, data_filename=None): - """ - Verify the signature on the contents of a file or file-like - object. Can handle embedded signatures as well as detached - signatures. If using detached signatures, the file containing the - detached signature should be specified as the :param:`data_filename`. - - @param file: A file descriptor object. Its type will be checked with - :func:`_is_file`. - @param data_filename: (optional) A file containing the GPG signature - data for :param:`file`. If given, :param:`file` - is verified via this detached signature. - """ - ## attempt to wrap any escape characters in quotes: - safe_file = _fix_unsafe(file) - - ## check that :param:`file` is actually a file: - _is_file(safe_file) - - logger.debug('verify_file: %r, %r', safe_file, data_filename) - result = self.result_map['verify'](self) - args = ['--verify'] - if data_filename is None: - self._handle_io(args, safe_file, result, binary=True) - else: - safe_data_filename = _fix_unsafe(data_filename) - - logger.debug('Handling detached verification') - fd, fn = tempfile.mkstemp(prefix='pygpg') - - with open(safe_file) as sf: - contents = sf.read() - os.write(fd, s) - os.close(fd) - logger.debug('Wrote to temp file: %r', contents) - args.append(fn) - args.append('"%s"' % safe_data_filename) - - try: - p = self._open_subprocess(args) - self._collect_output(p, result, stdin=p.stdin) - finally: - os.unlink(fn) - - return result - - # - # KEY MANAGEMENT - # - def import_keys(self, key_data): - """ - Import the key_data into our keyring. - - >>> import shutil - >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> print1 = result.fingerprint - >>> result = gpg.gen_key(input) - >>> print2 = result.fingerprint - >>> pubkey1 = gpg.export_keys(print1) - >>> seckey1 = gpg.export_keys(print1,secret=True) - >>> seckeys = gpg.list_keys(secret=True) - >>> pubkeys = gpg.list_keys() - >>> assert print1 in seckeys.fingerprints - >>> assert print1 in pubkeys.fingerprints - >>> str(gpg.delete_keys(print1)) - 'Must delete secret key first' - >>> str(gpg.delete_keys(print1,secret=True)) - 'ok' - >>> str(gpg.delete_keys(print1)) - 'ok' - >>> str(gpg.delete_keys("nosuchkey")) - 'No such key' - >>> seckeys = gpg.list_keys(secret=True) - >>> pubkeys = gpg.list_keys() - >>> assert not print1 in seckeys.fingerprints - >>> assert not print1 in pubkeys.fingerprints - >>> result = gpg.import_keys('foo') - >>> assert not result - >>> result = gpg.import_keys(pubkey1) - >>> pubkeys = gpg.list_keys() - >>> seckeys = gpg.list_keys(secret=True) - >>> assert not print1 in seckeys.fingerprints - >>> assert print1 in pubkeys.fingerprints - >>> result = gpg.import_keys(seckey1) - >>> assert result - >>> seckeys = gpg.list_keys(secret=True) - >>> pubkeys = gpg.list_keys() - >>> assert print1 in seckeys.fingerprints - >>> assert print1 in pubkeys.fingerprints - >>> assert print2 in pubkeys.fingerprints - """ - ## xxx need way to validate that key_data is actually a valid GPG key - ## it might be possible to use --list-packets and parse the output - - result = self.result_map['import'](self) - logger.debug('import_keys: %r', key_data[:256]) - data = _make_binary_stream(key_data, self.encoding) - self._handle_io(['--import'], data, result, binary=True) - logger.debug('import_keys result: %r', result.__dict__) - data.close() - return result - - def recv_keys(self, keyserver, *keyids): - """Import a key from a keyserver - - >>> import shutil - >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") - >>> result = gpg.recv_keys('pgp.mit.edu', '3FF0DB166A7476EA') - >>> assert result - - """ - safe_keyserver = _fix_unsafe(keyserver) - - result = self.result_map['import'](self) - data = _make_binary_stream("", self.encoding) - args = ['--keyserver', keyserver, '--recv-keys'] - - if keyids: - if keyids is not None: - safe_keyids = ' '.join( - [(lambda: _fix_unsafe(k))() for k in keyids]) - logger.debug('recv_keys: %r', safe_keyids) - args.extend(safe_keyids) - - self._handle_io(args, data, result, binary=True) - data.close() - logger.debug('recv_keys result: %r', result.__dict__) - return result - - def delete_keys(self, fingerprints, secret=False): - which='key' - if secret: - which='secret-key' - if _is_sequence(fingerprints): - fingerprints = ' '.join(fingerprints) - args = ['--batch --delete-%s "%s"' % (which, fingerprints)] - result = self.result_map['delete'](self) - p = self._open_subprocess(args) - self._collect_output(p, result, stdin=p.stdin) - return result - - def export_keys(self, keyids, secret=False): - "export the indicated keys. 'keyid' is anything gpg accepts" - which='' - if secret: - which='-secret-key' - if _is_sequence(keyids): - keyids = ' '.join(['"%s"' % k for k in keyids]) - args = ["--armor --export%s %s" % (which, keyids)] - p = self._open_subprocess(args) - # gpg --export produces no status-fd output; stdout will be - # empty in case of failure - #stdout, stderr = p.communicate() - result = self.result_map['delete'](self) # any result will do - self._collect_output(p, result, stdin=p.stdin) - logger.debug('export_keys result: %r', result.data) - return result.data.decode(self.encoding, self.decode_errors) - - def list_keys(self, secret=False): - """List the keys currently in the keyring. - - >>> import shutil - >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> print1 = result.fingerprint - >>> result = gpg.gen_key(input) - >>> print2 = result.fingerprint - >>> pubkeys = gpg.list_keys() - >>> assert print1 in pubkeys.fingerprints - >>> assert print2 in pubkeys.fingerprints - - """ - - which='keys' - if secret: - which='secret-keys' - args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which,) - args = [args] - p = self._open_subprocess(args) - - # there might be some status thingumy here I should handle... (amk) - # ...nope, unless you care about expired sigs or keys (stevegt) - - # Get the response information - result = self.result_map['list'](self) - self._collect_output(p, result, stdin=p.stdin) - lines = result.data.decode(self.encoding, - self.decode_errors).splitlines() - valid_keywords = 'pub uid sec fpr sub'.split() - for line in lines: - if self.verbose: - print(line) - logger.debug("line: %r", line.rstrip()) - if not line: - break - L = line.strip().split(':') - if not L: - continue - keyword = L[0] - if keyword in valid_keywords: - getattr(result, keyword)(L) - return result - - def gen_key(self, input): - """ - Generate a key; you might use gen_key_input() to create the control - input. - - >>> gpg = GPG(gpghome="keys") - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> assert result - >>> result = gpg.gen_key('foo') - >>> assert not result - - """ - args = ["--gen-key --batch"] - result = self.result_map['generate'](self) - f = _make_binary_stream(input, self.encoding) - self._handle_io(args, f, result, binary=True) - f.close() - return result - - def gen_key_input(self, **kwargs): - """ - Generate --gen-key input per gpg doc/DETAILS - """ - parms = {} - for key, val in list(kwargs.items()): - key = key.replace('_','-').title() - if str(val).strip(): # skip empty strings - parms[key] = val - parms.setdefault('Key-Type', 'RSA') - parms.setdefault('Key-Length', 2048) - parms.setdefault('Name-Real', "Autogenerated Key") - parms.setdefault('Name-Comment', "Generated by gnupg.py") - try: - logname = os.environ['LOGNAME'] - except KeyError: - logname = os.environ['USERNAME'] - hostname = socket.gethostname() - parms.setdefault('Name-Email', "%s@%s" - % (logname.replace(' ', '_'), hostname)) - out = "Key-Type: %s\n" % parms.pop('Key-Type') - for key, val in list(parms.items()): - out += "%s: %s\n" % (key, val) - out += "%commit\n" - return out - - # Key-Type: RSA - # Key-Length: 1024 - # Name-Real: ISdlink Server on %s - # Name-Comment: Created by %s - # Name-Email: isdlink@%s - # Expire-Date: 0 - # %commit - # - # - # Key-Type: DSA - # Key-Length: 1024 - # Subkey-Type: ELG-E - # Subkey-Length: 1024 - # Name-Real: Joe Tester - # Name-Comment: with stupid passphrase - # Name-Email: joe@foo.bar - # Expire-Date: 0 - # Passphrase: abc - # %pubring foo.pub - # %secring foo.sec - # %commit - - # - # ENCRYPTION - # - def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False): - "Encrypt the message read from the file-like object 'file'" - args = ['--encrypt'] - if symmetric: - args = ['--symmetric'] - else: - args = ['--encrypt'] - if not _is_sequence(recipients): - recipients = (recipients,) - for recipient in recipients: - args.append('--recipient "%s"' % recipient) - if armor: # create ascii-armored output - set to False for binary output - args.append('--armor') - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) - if sign: - args.append('--sign --default-key "%s"' % sign) - if always_trust: - args.append("--always-trust") - result = self.result_map['crypt'](self) - self._handle_io(args, file, result, passphrase=passphrase, binary=True) - logger.debug('encrypt result: %r', result.data) - return result - - def encrypt(self, data, recipients, **kwargs): - """Encrypt the message contained in the string 'data' - - >>> import shutil - >>> if os.path.exists("keys"): - ... shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") - >>> input = gpg.gen_key_input(passphrase='foo') - >>> result = gpg.gen_key(input) - >>> print1 = result.fingerprint - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> print2 = result.fingerprint - >>> result = gpg.encrypt("hello",print2) - >>> message = str(result) - >>> assert message != 'hello' - >>> result = gpg.decrypt(message) - >>> assert result - >>> str(result) - 'hello' - >>> result = gpg.encrypt("hello again",print1) - >>> message = str(result) - >>> result = gpg.decrypt(message,passphrase='bar') - >>> result.status in ('decryption failed', 'bad passphrase') - True - >>> assert not result - >>> result = gpg.decrypt(message,passphrase='foo') - >>> result.status == 'decryption ok' - True - >>> str(result) - 'hello again' - >>> result = gpg.encrypt("signed hello",print2,sign=print1,passphrase='foo') - >>> result.status == 'encryption ok' - True - >>> message = str(result) - >>> result = gpg.decrypt(message) - >>> result.status == 'decryption ok' - True - >>> assert result.fingerprint == print1 - - """ - data = _make_binary_stream(data, self.encoding) - result = self.encrypt_file(data, recipients, **kwargs) - data.close() - return result - - def decrypt(self, message, **kwargs): - data = _make_binary_stream(message, self.encoding) - result = self.decrypt_file(data, **kwargs) - data.close() - return result - - def decrypt_file(self, file, always_trust=False, passphrase=None, - output=None): - args = ["--decrypt"] - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) - if always_trust: - args.append("--always-trust") - result = self.result_map['crypt'](self) - self._handle_io(args, file, result, passphrase, binary=True) - logger.debug('decrypt result: %r', result.data) - return result - diff --git a/.pc/applied-patches b/.pc/applied-patches deleted file mode 100644 index 572d722..0000000 --- a/.pc/applied-patches +++ /dev/null @@ -1 +0,0 @@ -0001-Make-_open_subprocess-argument-more-explicit-in-_han.patch From d52c08bae751dc5b80e04c9e60605984738a8a14 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 06:20:01 +0000 Subject: [PATCH 003/145] Ignore distribute/setuptools PKG-INFO file. --- .gitignore | 3 +++ PKG-INFO | 29 ----------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) delete mode 100644 PKG-INFO diff --git a/.gitignore b/.gitignore index 0992271..1befb39 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ gpg # Ignore quilt patch directory: .pc/* + +# setuptools/distribute files: +PKG-INFO diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index 206db4c..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,29 +0,0 @@ -Metadata-Version: 1.1 -Name: python-gnupg -Version: 0.3.3 -Summary: A wrapper for the Gnu Privacy Guard (GPG or GnuPG) -Home-page: https://www.github.com/isislovecruft/python-gnupg -Author: Isis Lovecruft -Author-email: isis@leap.se -Download-URL: https://github.com/isislovecruft/python-gnupg/archives/develop.zip -Description:: This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.6 or greater. -Classifier:: Development Status :: 3 - Alpha -License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) -Operating System :: MacOS :: MacOS X -Operating System :: Microsoft :: Windows :: Windows 7 -Operating System :: Microsoft :: Windows :: Windows XP -Operating System :: POSIX :: BSD -Operating System :: POSIX :: Linux -Classifier:: Intended Audience :: Developers -Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) -Classifier:: Programming Language :: Python -Classifier:: Programming Language :: Python :: 2 -Classifier:: Programming Language :: Python :: 3 -Classifier:: Programming Language :: Python :: 2.6 -Classifier:: Programming Language :: Python :: 2.7 -Classifier:: Programming Language :: Python :: 3.0 -Classifier:: Programming Language :: Python :: 3.1 -Classifier:: Programming Language :: Python :: 3.2 -Classifier:: Topic :: Security :: Cryptography -Classifier:: Topic :: Software Development :: Libraries :: Python Modules -Classifier:: Topic :: Utilities From edaf6a138b21f6162ccbeca8fe0633a9fc385235 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 06:21:51 +0000 Subject: [PATCH 004/145] Ignore traceback records for tickets also. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1befb39..f9b641b 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ installed-files.txt # and git-tickets and tickets: .tickets/* tickets/* +TICKET-* # Ignore virtualenv folders, if they are here: include/* From 137dddd36cef44dbcdc2564cc571dc833b0144bd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 06:22:45 +0000 Subject: [PATCH 005/145] Ignore MANIFEST and MANIFEST.in files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f9b641b..b35b798 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ gpg # setuptools/distribute files: PKG-INFO +MANIFEST* From e5634a890795a06ae1f6ea0fd2c99290f6b11087 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:27:43 +0000 Subject: [PATCH 006/145] Add .PHONY alias to Makefile. --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 4571e0f..30c89d7 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +.PHONY=all +all: uninstall install test ctags: ctags -R *.py From 60a216a604b953265f4feb6c6b8c545c9056bd80 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:28:10 +0000 Subject: [PATCH 007/145] Fix a bug where "$ make cleantest" fails while removing tests/random_seed. --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 30c89d7..eeaf3a0 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,8 @@ cleanup-tests: rm tests/logs/placeholder.log touch placeholder.log rm *.log - rm tests/random_seed + touch tests/random_seed_is_sekritly_pi + rm tests/random_seed* cleanup-tests-all: cleanup-tests rm -rf tests/tmp From 6f9c4c74230746321b49e8ebaf4bc046f671ad8d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:31:04 +0000 Subject: [PATCH 008/145] Remove commented out code and unused pprint import. --- src/gnupg.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 2b1c647..caafbf4 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -79,7 +79,6 @@ except ImportError: from cStringIO import StringIO from codecs import open as open -from pprint import pprint from psutil import process_iter from subprocess import Popen from subprocess import PIPE @@ -843,8 +842,6 @@ use_agent: %s log.info('Importing: %r', key_data[:256]) data = _make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) - #pretty = pprint(result.__dict__, indent=4, width=76, depth=8) - #log.debug("Import result:%s%s" % (os.linesep, pretty)) data.close() return result From 72ef15f5a465a228e80462b39e240dcb9e51536d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:31:52 +0000 Subject: [PATCH 009/145] Remove unused logging and tempfile imports from gnupg.py. --- src/gnupg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index caafbf4..e2493ce 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -90,11 +90,9 @@ import codecs ## See https://code.patternsinthevoid.net/?p=android-locale-hack.git import encodings import locale -import logging import os import re import sys -import tempfile import threading from _parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list From ecd8faa4257d00e73bfde946fc345d2e0491dfc1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:33:38 +0000 Subject: [PATCH 010/145] Move re import to GPGWrapper class, it isn't used anywhere else. --- src/gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index e2493ce..ab3d306 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -91,7 +91,6 @@ import codecs import encodings import locale import os -import re import sys import threading @@ -1575,6 +1574,8 @@ class GPGWrapper(GPG): This is a temporary class for handling GPG requests, and should be replaced by a more general class used throughout the project. """ + import re + def find_key_by_email(self, email, secret=False): """ Find user's key based on their email. From a95a49e04ae3b9b64d7e50bae98bf5d692820d05 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:35:15 +0000 Subject: [PATCH 011/145] Move GPGMeta and GPGBase to _meta.py. --- src/_meta.py | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/gnupg.py | 256 -------------------------------------------- 2 files changed, 294 insertions(+), 256 deletions(-) create mode 100644 src/_meta.py diff --git a/src/_meta.py b/src/_meta.py new file mode 100644 index 0000000..c3dc6c0 --- /dev/null +++ b/src/_meta.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. + + +from psutil import process_iter + +import atexit +import encodings +## For AOS, the locale module will need to point to a wrapper around the +## java.util.Locale class. +## See https://code.patternsinthevoid.net/?p=android-locale-hack.git +import locale +import os +import sys + +import _util + +from _parsers import _check_preferences +from _parsers import _fix_unsafe +from _parsers import _sanitise +from _util import log +from _util import _conf + + +class GPGMeta(type): + """Metaclass for changing the :meth:GPG.__init__ initialiser. + + Detects running gpg-agent processes and the presence of a pinentry + program, and disables pinentry so that python-gnupg can write the + passphrase to the controlled GnuPG process without killing the agent. + """ + + def __new__(cls, name, bases, attrs): + """Construct the initialiser for GPG""" + log.debug("Metaclass __new__ constructor called for %r" % cls) + if cls._find_agent(): + ## call the normal GPG.__init__() initialisor: + attrs['init'] = cls.__init__ ## nothing changed for now + attrs['_remove_agent'] = True + return super(GPGMeta, cls).__new__(cls, name, bases, attrs) + + @classmethod + def _find_agent(cls): + """Discover if a gpg-agent process for the current euid is running. + + If there is a matching gpg-agent process, set a :class:psutil.Process + instance containing the gpg-agent process' information to + :attr:cls._agent_proc. + + :returns: True if there exists a gpg-agent process running under the + same effective user ID as that of this program. Otherwise, + returns None. + """ + identity = os.getresuid() + for proc in process_iter(): + if (proc.name == "gpg-agent") and proc.is_running: + log.debug("Found gpg-agent process with pid %d" % proc.pid) + if proc.uids == identity: + log.debug( + "Effective UIDs of this process and gpg-agent match") + setattr(cls, '_agent_proc', proc) + return True + + +class GPGBase(object): + """Base class to control process initialisation and for property storage.""" + + __metaclass__ = GPGMeta + + def __init__(self, binary=None, home=None, keyring=None, secring=None, + use_agent=False, default_preference_list=None, + verbose=False, options=None): + + self.binary = _util._find_binary(binary) + self.homedir = home if home else _conf + pub = _fix_unsafe(keyring) if keyring else 'pubring.gpg' + sec = _fix_unsafe(secring) if secring else 'secring.gpg' + self.keyring = os.path.join(self._homedir, pub) + self.secring = os.path.join(self._homedir, sec) + self.options = _sanitise(options) if options else None + + if default_preference_list: + self._prefs = _check_preferences(default_preference_list, 'all') + else: + self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH' + self._prefs += ' AES192 ZLIB ZIP Uncompressed' + + encoding = locale.getpreferredencoding() + if encoding is None: # This happens on Jython! + encoding = sys.stdin.encoding + self.encoding = encoding.lower().replace('-', '_') + self.filesystemencoding = encodings.normalize_encoding( + sys.getfilesystemencoding().lower()) + + try: + assert self.binary, "Could not find binary %s" % binary + assert isinstance(verbose, (bool, str, int)), \ + "'verbose' must be boolean, string, or 0 <= n <= 9" + assert isinstance(use_agent, bool), "'use_agent' must be boolean" + if self.options is not None: + assert isinstance(self.options, str), "options not string" + except (AssertionError, AttributeError) as ae: + log.error("GPGBase.__init__(): %s" % ae.message) + raise RuntimeError(ae.message) + else: + self.verbose = verbose + self.use_agent = use_agent + + if hasattr(self, '_agent_proc') \ + and getattr(self, '_remove_agent', None) is True: + if hasattr(self, '__remove_path__'): + self.__remove_path__('pinentry') + + def __remove_path__(self, prog=None, at_exit=True): + """Remove a the directories containing a program from the system's + $PATH. If self.gpg.binary is in a directory being removed, it is + symlinked to './gpg' + + :param str prog: + """ + self._removed_path_entries = [] + + log.debug("Attempting to remove %s from system PATH" % str(prog)) + if (prog is None) or (not isinstance(prog, str)): return + + try: + program = _util._which(prog)[0] + except (OSError, IOError, IndexError) as err: + log.err(err.message) + log.err("Cannot find program '%s', not changing PATH." % prog) + return + + ## __remove_path__ cannot be an @classmethod in GPGMeta, because + ## the use_agent attribute must be set by the instance. + if not self.use_agent: + program_base = os.path.dirname(prog) + gnupg_base = os.path.dirname(self.binary) + + ## symlink our gpg binary into $PWD if the path we are removing is + ## the one which contains our gpg executable: + if gnupg_base == program_base: + os.symlink(self.binary, os.path.join(os.getcwd(), 'gpg')) + + ## copy the original environment so that we can put it back later: + env_copy = os.environ ## this one should not be touched + path_copy = os.environ.pop('PATH') + log.debug("Created a copy of system PATH: %r" % path_copy) + assert not os.environ.has_key('PATH'), "OS env kept $PATH anyway!" + + @staticmethod + def remove_program_from_path(path, prog_base): + """Remove all directories which contain a program from PATH. + + :param str path: The contents of the system environment's + PATH. + :param str prog_base: The base (directory only) portion of a + program's location. + """ + paths = path.split(':') + for directory in paths: + if directory == prog_base: + log.debug("Found directory with target program: %s" + % directory) + path.remove(directory) + self._removed_path_entries.append(directory) + log.debug("Deleted all found instance of %s." % directory) + log.debug("PATH is now:%s%s" % (os.linesep, path)) + new_path = ':'.join([p for p in path]) + return new_path + + @staticmethod + def update_path(environment, path): + """Add paths to the string at os.environ['PATH']. + + :param str environment: The environment mapping to update. + :param list path: A list of strings to update the PATH with. + """ + log.debug("Updating system path...") + os.environ = environment + new_path = ':'.join([p for p in path]) + old = '' + if 'PATH' in os.environ: + new_path = ':'.join([os.environ['PATH'], new_path]) + os.environ.update({'PATH': new_path}) + log.debug("System $PATH: %s" % os.environ['PATH']) + + modified_path = remove_program_from_path(path_copy, program_base) + update_path(env_copy, modified_path) + + ## register an _exithandler with the python interpreter: + atexit.register(update_path, env_copy, path_copy) + + @atexit.register + def remove_symlinked_binary(): + loc = os.path.join(os.getcwd(), 'gpg') + if os.path.islink(loc): + os.unline(loc) + log.debug("Removed binary symlink '%s'" % loc) + + @property + def default_preference_list(self): + """Get the default preference list.""" + return self._prefs + + @default_preference_list.setter + def default_preference_list(self, prefs): + """Set the default preference list. + + :param str prefs: A string containing the default preferences for + ciphers, digests, and compression algorithms. + """ + prefs = _check_preferences(prefs) + if prefs is not None: + self._prefs = prefs + + @default_preference_list.deleter + def default_preference_list(self, prefs): + """Reset the default preference list to its original state. + + Note that "original state" does not mean the default preference + list for whichever version of GnuPG is being used. It means the + default preference list defined by :attr:`GPGBase._preferences`. + + Using BZIP2 is avoided due to not interacting well with some versions + of GnuPG>=2.0.0. + """ + self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' + + def _homedir_getter(self): + """Get the directory currently being used as GnuPG's homedir. + + If unspecified, use $HOME/.config/python-gnupg/ + + :rtype: str + :returns: The absolute path to the current GnuPG homedir. + """ + return self._homedir + + def _homedir_setter(self, directory): + """Set the directory to use as GnuPG's homedir. + + If unspecified, use $HOME/.config/python-gnupg. If specified, ensure + that the ``directory`` does not contain various shell escape + characters. If ``directory`` is not found, it will be automatically + created. Lastly, the ``direcory`` will be checked that the EUID has + read and write permissions for it. + + :param str homedir: A relative or absolute path to the directory to use + for storing/accessing GnuPG's files, including + keyrings and the trustdb. + :raises: :exc:`RuntimeError` if unable to find a suitable directory to + use. + """ + if not directory: + log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'" + % _conf) + directory = _conf + + hd = _fix_unsafe(directory) + log.debug("GPGBase._homedir_setter(): got directory '%s'" % hd) + + if hd: + log.debug("GPGBase._homedir_setter(): Check existence of '%s'" % hd) + _util._create_if_necessary(hd) + + try: + log.debug("GPGBase._homedir_setter(): checking permissions") + assert _util._has_readwrite(hd), \ + "Homedir '%s' needs read/write permissions" % hd + except AssertionError as ae: + msg = ("Unable to set '%s' as GnuPG homedir" % directory) + log.debug("GPGBase.homedir.setter(): %s" % msg) + log.debug(ae.message) + raise RuntimeError(ae.message) + else: + log.info("Setting homedir to '%s'" % hd) + self._homedir = hd + + homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter) diff --git a/src/gnupg.py b/src/gnupg.py index ab3d306..2c818cf 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -104,262 +104,6 @@ import _util import _parsers -class GPGMeta(type): - """Metaclass for changing the :meth:GPG.__init__ initialiser. - - Detects running gpg-agent processes and the presence of a pinentry - program, and disables pinentry so that python-gnupg can write the - passphrase to the controlled GnuPG process without killing the agent. - """ - - def __new__(cls, name, bases, attrs): - """Construct the initialiser for GPG""" - log.debug("Metaclass __new__ constructor called for %r" % cls) - if cls._find_agent(): - ## call the normal GPG.__init__() initialisor: - attrs['init'] = cls.__init__ ## nothing changed for now - attrs['_remove_agent'] = True - return super(GPGMeta, cls).__new__(cls, name, bases, attrs) - - @classmethod - def _find_agent(cls): - """Discover if a gpg-agent process for the current euid is running. - - If there is a matching gpg-agent process, set a :class:psutil.Process - instance containing the gpg-agent process' information to - :attr:cls._agent_proc. - - :returns: True if there exists a gpg-agent process running under the - same effective user ID as that of this program. Otherwise, - returns None. - """ - identity = os.getresuid() - for proc in process_iter(): - if (proc.name == "gpg-agent") and proc.is_running: - log.debug("Found gpg-agent process with pid %d" % proc.pid) - if proc.uids == identity: - log.debug( - "Effective UIDs of this process and gpg-agent match") - setattr(cls, '_agent_proc', proc) - return True - - -class GPGBase(object): - """Base class to control process initialisation and for property storage.""" - - __metaclass__ = GPGMeta - - def __init__(self, binary=None, home=None, keyring=None, secring=None, - use_agent=False, default_preference_list=None, - verbose=False, options=None): - - self.binary = _util._find_binary(binary) - self.homedir = home if home else _conf - pub = _fix_unsafe(keyring) if keyring else 'pubring.gpg' - sec = _fix_unsafe(secring) if secring else 'secring.gpg' - self.keyring = os.path.join(self._homedir, pub) - self.secring = os.path.join(self._homedir, sec) - self.options = _sanitise(options) if options else None - - if default_preference_list: - self._prefs = _check_options(default_preference_list, 'all') - else: - self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH' - self._prefs += ' AES192 ZLIB ZIP Uncompressed' - - encoding = locale.getpreferredencoding() - if encoding is None: # This happens on Jython! - encoding = sys.stdin.encoding - self.encoding = encoding.lower().replace('-', '_') - self.filesystemencoding = encodings.normalize_encoding( - sys.getfilesystemencoding().lower()) - - try: - assert self.binary, "Could not find binary %s" % binary - assert isinstance(verbose, (bool, str, int)), \ - "'verbose' must be boolean, string, or 0 <= n <= 9" - assert isinstance(use_agent, bool), "'use_agent' must be boolean" - if self.options is not None: - assert isinstance(self.options, str), "options not string" - except (AssertionError, AttributeError) as ae: - log.error("GPGBase.__init__(): %s" % ae.message) - raise RuntimeError(ae.message) - else: - self.verbose = verbose - self.use_agent = use_agent - - if hasattr(self, '_agent_proc') \ - and getattr(self, '_remove_agent', None) is True: - if hasattr(self, '__remove_path__'): - self.__remove_path__('pinentry') - - def __remove_path__(self, prog=None, at_exit=True): - """Remove a the directories containing a program from the system's - $PATH. If self.gpg.binary is in a directory being removed, it is - symlinked to './gpg' - - :param str prog: - """ - self._removed_path_entries = [] - - log.debug("Attempting to remove %s from system PATH" % str(prog)) - if (prog is None) or (not isinstance(prog, str)): return - - try: - program = _util._which(prog)[0] - except (OSError, IOError, IndexError) as err: - log.err(err.message) - log.err("Cannot find program '%s', not changing PATH." % prog) - return - - ## __remove_path__ cannot be an @classmethod in GPGMeta, because - ## the use_agent attribute must be set by the instance. - if not self.use_agent: - program_base = os.path.dirname(prog) - gnupg_base = os.path.dirname(self.binary) - - ## symlink our gpg binary into $PWD if the path we are removing is - ## the one which contains our gpg executable: - if gnupg_base == program_base: - os.symlink(self.binary, os.path.join(os.getcwd(), 'gpg')) - - ## copy the original environment so that we can put it back later: - env_copy = os.environ ## this one should not be touched - path_copy = os.environ.pop('PATH') - log.debug("Created a copy of system PATH: %r" % path_copy) - assert not os.environ.has_key('PATH'), "OS env kept $PATH anyway!" - - @staticmethod - def remove_program_from_path(path, prog_base): - """Remove all directories which contain a program from PATH. - - :param str path: The contents of the system environment's - PATH. - :param str prog_base: The base (directory only) portion of a - program's location. - """ - paths = path.split(':') - for directory in paths: - if directory == prog_base: - log.debug("Found directory with target program: %s" - % directory) - path.remove(directory) - self._removed_path_entries.append(directory) - log.debug("Deleted all found instance of %s." % directory) - log.debug("PATH is now:%s%s" % (os.linesep, path)) - new_path = ':'.join([p for p in path]) - return new_path - - @staticmethod - def update_path(environment, path): - """Add paths to the string at os.environ['PATH']. - - :param str environment: The environment mapping to update. - :param list path: A list of strings to update the PATH with. - """ - log.debug("Updating system path...") - os.environ = environment - new_path = ':'.join([p for p in path]) - old = '' - if 'PATH' in os.environ: - new_path = ':'.join([os.environ['PATH'], new_path]) - os.environ.update({'PATH': new_path}) - log.debug("System $PATH: %s" % os.environ['PATH']) - - modified_path = remove_program_from_path(path_copy, program_base) - update_path(env_copy, modified_path) - - ## register an _exithandler with the python interpreter: - atexit.register(update_path, env_copy, path_copy) - - @atexit.register - def remove_symlinked_binary(): - loc = os.path.join(os.getcwd(), 'gpg') - if os.path.islink(loc): - os.unline(loc) - log.debug("Removed binary symlink '%s'" % loc) - - @property - def default_preference_list(self): - """Get the default preference list.""" - return self._prefs - - @default_preference_list.setter - def default_preference_list(self, prefs): - """Set the default preference list. - - :param str prefs: A string containing the default preferences for - ciphers, digests, and compression algorithms. - """ - prefs = _check_preferences(prefs) - if prefs is not None: - self._prefs = prefs - - @default_preference_list.deleter - def default_preference_list(self, prefs): - """Reset the default preference list to its original state. - - Note that "original state" does not mean the default preference - list for whichever version of GnuPG is being used. It means the - default preference list defined by :attr:`GPGBase._preferences`. - - Using BZIP2 is avoided due to not interacting well with some versions - of GnuPG>=2.0.0. - """ - self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' - - def _homedir_getter(self): - """Get the directory currently being used as GnuPG's homedir. - - If unspecified, use $HOME/.config/python-gnupg/ - - :rtype: str - :returns: The absolute path to the current GnuPG homedir. - """ - return self._homedir - - def _homedir_setter(self, directory): - """Set the directory to use as GnuPG's homedir. - - If unspecified, use $HOME/.config/python-gnupg. If specified, ensure - that the ``directory`` does not contain various shell escape - characters. If ``directory`` is not found, it will be automatically - created. Lastly, the ``direcory`` will be checked that the EUID has - read and write permissions for it. - - :param str homedir: A relative or absolute path to the directory to use - for storing/accessing GnuPG's files, including - keyrings and the trustdb. - :raises: :exc:`RuntimeError` if unable to find a suitable directory to - use. - """ - if not directory: - log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'" - % _conf) - directory = _conf - - hd = _fix_unsafe(directory) - log.debug("GPGBase._homedir_setter(): got directory '%s'" % hd) - - if hd: - log.debug("GPGBase._homedir_setter(): Check existence of '%s'" % hd) - _util._create_if_necessary(hd) - - try: - log.debug("GPGBase._homedir_setter(): checking permissions") - assert _util._has_readwrite(hd), \ - "Homedir '%s' needs read/write permissions" % hd - except AssertionError as ae: - msg = ("Unable to set '%s' as GnuPG homedir" % directory) - log.debug("GPGBase.homedir.setter(): %s" % msg) - log.debug(ae.message) - raise RuntimeError(ae.message) - else: - log.info("Setting homedir to '%s'" % hd) - self._homedir = hd - - homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter) - class GPG(GPGBase): """Encapsulate access to the gpg executable""" From bdd08fb31fa0ec22681daec78fba5c67c5f663be Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:39:02 +0000 Subject: [PATCH 012/145] Remove GPGMeta and GPGBase specific imports from gnupg.py. --- src/gnupg.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 2c818cf..5c2b87d 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -79,25 +79,16 @@ except ImportError: from cStringIO import StringIO from codecs import open as open -from psutil import process_iter from subprocess import Popen from subprocess import PIPE -import atexit import codecs -## For AOS, the locale module will need to point to a wrapper around the -## java.util.Locale class. -## See https://code.patternsinthevoid.net/?p=android-locale-hack.git import encodings -import locale import os -import sys import threading -from _parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list -from _parsers import _check_preferences -from _util import _conf, _is_list_or_tuple, _is_stream -from _util import _make_binary_stream +from _parsers import _fix_unsafe, _sanitise_list +from _util import _is_list_or_tuple, _is_stream from _util import log import _util From 6f5c7fb610b907580070f9826694882216bb76c4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:40:32 +0000 Subject: [PATCH 013/145] Split imports in gnupg.py to one-per-line. --- src/gnupg.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 5c2b87d..e5fdb0c 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -87,13 +87,16 @@ import encodings import os import threading -from _parsers import _fix_unsafe, _sanitise_list -from _util import _is_list_or_tuple, _is_stream -from _util import log - -import _util import _parsers +import _util +from _meta import GPGBase +from _parsers import _fix_unsafe +from _parsers import _sanitise_list +from _util import _is_list_or_tuple +from _util import _is_stream +from _util import _make_binary_stream +from _util import log class GPG(GPGBase): From 18f527c514a09b92cf388d76c2eb0863949503a4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:40:55 +0000 Subject: [PATCH 014/145] Add a missing colon in an attribute autodoc comment. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index e5fdb0c..09973b4 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -112,7 +112,7 @@ class GPG(GPGBase): 'verify': _parsers.Verify, 'packets': _parsers.ListPackets } #: The number of simultaneous keyids we should list operations like - # '--list-sigs' to: + #: '--list-sigs' to: _batch_limit = 25 def __init__(self, binary=None, homedir=None, verbose=False, From 4c1d7599bdbb8fce29a661355b73546137baef71 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 07:41:32 +0000 Subject: [PATCH 015/145] Remove duplicate code from GPGWrapper. --- src/gnupg.py | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 09973b4..e0b74e7 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1345,15 +1345,6 @@ class GPGWrapper(GPG): symmetric=symmetric, cipher_algo='AES256') - def decrypt(self, data, always_trust=True, passphrase=None): - """ - Decrypt data using GPG. - """ - # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).decrypt(data, - always_trust=always_trust, - passphrase=passphrase) - def send_keys(self, keyserver, *keyids): """Send keys to a keyserver.""" result = self._result_map['list'](self) @@ -1366,45 +1357,6 @@ class GPGWrapper(GPG): data.close() return result - def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False, - cipher_algo=None): - "Encrypt the message read from the file-like object 'file'" - args = ['--encrypt'] - if symmetric: - args = ['--symmetric'] - if cipher_algo: - args.append('--cipher-algo %s' % cipher_algo) - else: - args = ['--encrypt'] - if not _util._is_list_or_tuple(recipients): - recipients = (recipients,) - for recipient in recipients: - args.append('--recipient "%s"' % recipient) - if armor: # create ascii-armored output - set to False for binary - args.append('--armor') - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) - if sign: - args.append('--sign --default-key "%s"' % sign) - if always_trust: - args.append("--always-trust") - result = self._result_map['crypt'](self) - self._handle_io(args, file, result, passphrase=passphrase, binary=True) - log.debug('encrypt result: %r', result.data) - return result - - def list_packets(self, raw_data): - args = ["--list-packets"] - result = self._result_map['list-packets'](self) - self._handle_io(args, - _util._make_binary_stream(raw_data, self.encoding), - result) - return result - def encrypted_to(self, raw_data): """ Return the key to which raw_data is encrypted to. From b7d9f749b69a5cbe1c497e7fae516e63f4326c9c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:04:47 +0000 Subject: [PATCH 016/145] Decrypt docs/NOTES-python-openpgp-implementations. --- docs/NOTES-python-openpgp-implementations.gpg | Bin 1291 -> 0 bytes docs/NOTES-python-openpgp-implementations.txt | 31 ++++++++++++++++++ 2 files changed, 31 insertions(+) delete mode 100644 docs/NOTES-python-openpgp-implementations.gpg create mode 100644 docs/NOTES-python-openpgp-implementations.txt diff --git a/docs/NOTES-python-openpgp-implementations.gpg b/docs/NOTES-python-openpgp-implementations.gpg deleted file mode 100644 index 0a6e020af7a5b54bc405bf6dd16ff856703b5d6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1291 zcmV+m1@!ub0t^FC$&H7!0T2%X5C3T(2tI*xUR^Y~kXKP6Whf49ikz+yphJ$MRUI6= zui(-{8;)P`scmE3)LuFtLD+>!|E0a8O`aLnrS7%msmf#JON>lO%2|J&&@In@mna?~ z4#wRFEv-I~v6rk0arelxFz`woPh8ZnQxUs8PJW->2o3hnwiU^|M8KQcMp%Acs!2Da z_7cvmX{ru*=?f_+0O>jcI=H%alja^Qm^qH_Y3WHJv=o5zVLIlWl3XkAGFk|K`xOxo zY*<&_SVG$!Gi^%oeAwer0%!))KW;9G`<<9i9!$|(OKW{2-hz!%eOiL)bxNwV%4;{$ zu3FtYtAgOJmpf)(U*HJU_ffsorJEG}f{ux;auk~nbe|PRHLNRs?WxV(-Axf&3*z3~ zQ5E6M7p`-avnZo@oPy{$`c@2WTQ&v|{^eX6Xe>MqP4CcpE_$uZ5{# zz?w3|5}HFsmCse20Uy7_K;pu%maKp{T8Vx*)I}bK8FWFnDS(U_tl3#{n5d!g8EA7Y z0nfI!bgts(J2|7s=f-5#Dc7oQp_hLjeqkj_E0k|7?)=4?trkl)h{oCU%h-np{%x~@ z)$|%2+EMzm?{Y&11eWbhAi-@*J`nFO{d>`v!Hocb3Mi)Ek)=i!?uRbX=;FN3M zJog!H$q^ml5e$pAF>0onDG}pU@D-6IzQ{JA7h^OEEC}2Ng7F#=xDmc8x%g>xd1fD5?%gK`7DKKn=<2q}%Xj&erz)e1)^~AVxHc8dtj!4h zLxZ?_L2z;n?&hBCxUWL&LUh!GwRUIi4bLSQH~K<)$b(;?zC11TfZeY#73f7x;4Wp| zj#C5eg~%)49S4`A1gbasCAIN;}=JI2(Zq>6mq}ywMcDet{rt@;X{fzU z;!%2EOg;y|k_8@%f#C1kex{chOr>LC<$ZCxbNP_9;eu%zEHfo^wNJZ$Gfr?T?^fTF Bc_07) diff --git a/docs/NOTES-python-openpgp-implementations.txt b/docs/NOTES-python-openpgp-implementations.txt new file mode 100644 index 0000000..bf60728 --- /dev/null +++ b/docs/NOTES-python-openpgp-implementations.txt @@ -0,0 +1,31 @@ +-*- mode: org -*- + +* Other Python OpenPGP libraries and utilities: + +*** pygpgme - https://launchpad.net/pygpgme +A limited set of Python wrappers around GPGME +http://www.gnupg.org/documentation/manuals/gpgme/ + +*** py-gnupg - https://github.com/kevinoid/py-gnupg/blob/master/GnuPGInterface.py +Focuses mainly on using file handles to interact with GnuPG. + +*** OpenPGP-Python - https://github.com/singpolyma/OpenPGP-Python +The commit messages are a bit worrysome and the code has some scary +error-prone-looking method chaining going on, a five minute glance over the +/OpenPGP/Crypto.py file and it appears this is actually a valid OpenPGP +implementation, built using D.Litzenberger's PyCrypto library. +https://github.com/dlitz/pycrypto + +This person also wrote OpenPGP-Haskell: +https://github.com/singpolyma/OpenPGP-Haskell + +...and OpenPGP-PHP (/horrorface): https://github.com/singpolyma/openpgp-php and +an HTTP server as a shell script with a pretty crazy pipe hack. + +...and kudos on this one, it's an attempt at a mnemnonic system for squaring +Zooko's Triangle (L17 being a function named "countLeadingCrapAndZeros"): +https://github.com/singpolyma/mnemonicode/blob/master/mnencode.c#L17 + +* GnuPG unattended key generation scripts: + +*** mandos-keygen http://bzr.recompile.se/loggerhead/mandos/trunk/annotate/523/mandos-keygen?start_revid=616 From eec650d13faf81b9a588c31f5b4abdcca21ebebc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:19:34 +0000 Subject: [PATCH 017/145] Add note in TODO about possible bug in GnuPG v1.4.12 with 'Key-Type: default'. --- TODO | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/TODO b/TODO index d752f6d..58e6520 100644 --- a/TODO +++ b/TODO @@ -44,6 +44,53 @@ would likely need to happen here [[gnupg.py:1146]]. This would be necessary for adding a passphrase to the key after passwordless generation in GnuPG>=2.1.0. +** TODO GnuPG==1.4.12 doesn't process "Key-Type: default" in batch files +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ ipython +WARNING: Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. + +In [1]: import gnupg +GnuPG logging disabled... + +In [2]: gpg = gnupg.GPG(homedir='./tests/doctests') + +In [3]: key_input = gpg.gen_key_input() + +In [4]: print key_input +Key-Type: default +Key-Length: 4096 +Subkey-Type: default +Name-Email: isis@wintermute +Expire-Date: 2014-05-28 +Name-Real: Autogenerated Key +%commit + + +In [5]: key = gpg.gen_key(key_input) + +In [6]: print key.stderr +gpg: -:1: invalid algorithm +[GNUPG:] KEY_NOT_CREATED + + +In [7]: quit() +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ which gpg +/usr/bin/gpg +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ gpg --version +gpg (GnuPG) 1.4.12 +Copyright (C) 2012 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Home: ~/.gnupg +Supported algorithms: +Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA +Cipher: 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, + CAMELLIA192, CAMELLIA256 +Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 +Compression: Uncompressed, ZIP, ZLIB, BZIP2 +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ + * Code cleanup :cleanup: ** TODO in parsers.__sanitise() :cleanup:sanitise: Ughh...this is the ugliest code I think I've ever written. It works, but I From f1d5622a136ae5f384608bc84b8e8e9fc4f2b72c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:26:46 +0000 Subject: [PATCH 018/145] Add placeholder for tests/doctests/ and add to .gitignore. --- .gitignore | 1 + tests/doctests/README | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 tests/doctests/README diff --git a/.gitignore b/.gitignore index b35b798..ece3706 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ tags *.secring *random_seed* *.log +tests/doctests/* # Ignore distutils record of installed files: installed-files.txt diff --git a/tests/doctests/README b/tests/doctests/README new file mode 100644 index 0000000..6b14ed4 --- /dev/null +++ b/tests/doctests/README @@ -0,0 +1,4 @@ +This directory contains files related to doctests in python-gnupg docstrings. + +This file is a placeholder so that this directory exists when doctests are +executed, and is not necessary to keep. It is safe to delete this directory. From 08bbfb3a687bfb48749f82a67f17f70d16e8734c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:37:46 +0000 Subject: [PATCH 019/145] Fix several missing 's's in doctest calls to GPG(homedir='./tests/doctests'). --- src/gnupg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index e0b74e7..76dd0a8 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -738,7 +738,7 @@ use_agent: %s """Get the signatures for each of the ``keyids``. >>> import gnupg - >>> gpg = gnupg.GPG(homedir="./tests/doctest") + >>> gpg = gnupg.GPG(homedir="./tests/doctests") >>> key_input = gpg.gen_key_input() >>> key = gpg.gen_key(key_input) >>> assert key.fingerprint @@ -768,7 +768,7 @@ use_agent: %s :meth:`GPG.gen_key_input()` for creating the control input. >>> import gnupg - >>> gpg = gnupg.GPG(homedir="./tests/doctest") + >>> gpg = gnupg.GPG(homedir="./tests/doctests") >>> key_input = gpg.gen_key_input() >>> key = gpg.gen_key(key_input) >>> assert key.fingerprint @@ -839,7 +839,7 @@ use_agent: %s >>> import gnupg GnuPG logging disabled... >>> from __future__ import print_function - >>> gpg = gnupg.GPG(homedir='./tests/doctest') + >>> gpg = gnupg.GPG(homedir='./tests/doctests') >>> alice = { 'name_real': 'Alice', ... 'name_email': 'alice@inter.net', ... 'expire_date': '2014-04-01', @@ -861,8 +861,8 @@ use_agent: %s Name-Email: alice@inter.net Key-Length: 4096 Subkey-Length: 4096 - %pubring ./tests/doctest/pubring.gpg - %secring ./tests/doctest/secring.gpg + %pubring ./tests/doctests/pubring.gpg + %secring ./tests/doctests/secring.gpg %commit >>> alice_key = gpg.gen_key(alice_input) From 5b731736fea815ef16446960f1e729564f7bdecc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:42:03 +0000 Subject: [PATCH 020/145] Remove duplicate attribute primary_key_created from GenKey. --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 3d5c1d5..25c81d4 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -706,7 +706,7 @@ class GenKey(object): raise ValueError("Unknown status message: %r" % key) if self.type in ('B', 'P'): - self.primary_key_created = True + self.primary_created = True if self.type in ('B', 'S'): self.subkey_created = True From 87ced52f85ea7ceaacb2b4df62acad8d0e379274 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:48:36 +0000 Subject: [PATCH 021/145] =?UTF-8?q?GPG.encoding=20=E2=86=92=20GPG.=5Fencod?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_meta.py | 2 +- src/_parsers.py | 4 ++-- src/gnupg.py | 30 +++++++++++++++--------------- tests/test_gnupg.py | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/_meta.py b/src/_meta.py index c3dc6c0..1a0c7d4 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -103,7 +103,7 @@ class GPGBase(object): encoding = locale.getpreferredencoding() if encoding is None: # This happens on Jython! encoding = sys.stdin.encoding - self.encoding = encoding.lower().replace('-', '_') + self._encoding = encoding.lower().replace('-', '_') self.filesystemencoding = encodings.normalize_encoding( sys.getfilesystemencoding().lower()) diff --git a/src/_parsers.py b/src/_parsers.py index 25c81d4..8cd8059 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -766,7 +766,7 @@ class Sign(object): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) + return self.data.decode(self.gpg._encoding, self.gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. @@ -1101,7 +1101,7 @@ class Crypt(Verify): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) + return self.data.decode(self.gpg._encoding, self.gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. diff --git a/src/gnupg.py b/src/gnupg.py index 76dd0a8..d931256 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -339,7 +339,7 @@ use_agent: %s make sure it's joined before returning. If a stdin stream is given, close it before returning. """ - stderr = codecs.getreader(self.encoding)(process.stderr) + stderr = codecs.getreader(self._encoding)(process.stderr) rr = threading.Thread(target=self._read_response, args=(stderr, result)) rr.setDaemon(True) log.debug('stderr reader: %r', rr) @@ -368,11 +368,11 @@ use_agent: %s """Handle a call to GPG - pass input data, collect output data.""" p = self._open_subprocess(args, passphrase) if not binary: - stdin = codecs.getwriter(self.encoding)(p.stdin) + stdin = codecs.getwriter(self._encoding)(p.stdin) else: stdin = p.stdin if passphrase: - _util._write_passphrase(stdin, passphrase, self.encoding) + _util._write_passphrase(stdin, passphrase, self._encoding) writer = _util._threaded_copy_data(file, stdin) self._collect_output(p, result, writer, stdin) return result @@ -421,7 +421,7 @@ use_agent: %s % (data, kwargs[keyid])) else: log.warn("No 'sign_with' keyid given! Using default key.") - stream = _make_binary_stream(data, self.encoding) + stream = _make_binary_stream(data, self._encoding) result = self._sign_file(stream, **kwargs) stream.close() @@ -467,7 +467,7 @@ use_agent: %s proc = self._open_subprocess(args, passphrase is not None) try: if passphrase: - _util._write_passphrase(proc.stdin, passphrase, self.encoding) + _util._write_passphrase(proc.stdin, passphrase, self._encoding) writer = _util._threaded_copy_data(file, proc.stdin) except IOError as ioe: log.exception("Error writing message: %s" % ioe.message) @@ -490,7 +490,7 @@ use_agent: %s >>> assert verify """ - f = _make_binary_stream(data, self.encoding) + f = _make_binary_stream(data, self._encoding) result = self.verify_file(f) f.close() return result @@ -575,7 +575,7 @@ use_agent: %s result = self._result_map['import'](self) log.info('Importing: %r', key_data[:256]) - data = _make_binary_stream(key_data, self.encoding) + data = _make_binary_stream(key_data, self._encoding) self._handle_io(['--import'], data, result, binary=True) data.close() return result @@ -593,7 +593,7 @@ use_agent: %s safe_keyserver = _fix_unsafe(keyserver) result = self._result_map['import'](self) - data = _make_binary_stream("", self.encoding) + data = _make_binary_stream("", self._encoding) args = ['--keyserver', keyserver, '--recv-keys'] if keyids: @@ -672,7 +672,7 @@ use_agent: %s result = self._result_map['delete'](self) # any result will do self._collect_output(p, result, stdin=p.stdin) log.debug('Exported:%s%r' % (os.linesep, result.data)) - return result.data.decode(self.encoding, self._decode_errors) + return result.data.decode(self._encoding, self._decode_errors) def list_keys(self, secret=False): """List the keys currently in the keyring. @@ -709,7 +709,7 @@ use_agent: %s # Get the response information result = self._result_map['list'](self) self._collect_output(p, result, stdin=p.stdin) - lines = result.data.decode(self.encoding, + lines = result.data.decode(self._encoding, self._decode_errors).splitlines() valid_keywords = 'pub uid sec fpr sub'.split() for line in lines: @@ -730,7 +730,7 @@ use_agent: %s """List the packet contents of a file.""" args = ["--list-packets"] result = self._result_map['packets'](self) - self._handle_io(args, _make_binary_stream(raw_data, self.encoding), + self._handle_io(args, _make_binary_stream(raw_data, self._encoding), result) return result @@ -781,7 +781,7 @@ use_agent: %s ## see TODO file, tag :gen_key: for todo items args = ["--gen-key --batch"] key = self._result_map['generate'](self) - f = _make_binary_stream(input, self.encoding) + f = _make_binary_stream(input, self._encoding) self._handle_io(args, f, key, binary=True) f.close() @@ -1269,7 +1269,7 @@ generate keys. Please see >>> assert result.fingerprint == print1 """ - stream = _make_binary_stream(data, self.encoding) + stream = _make_binary_stream(data, self._encoding) result = self.encrypt_file(stream, recipients, **kwargs) stream.close() return result @@ -1279,7 +1279,7 @@ generate keys. Please see :param message: A string or file-like object to decrypt. """ - stream = _make_binary_stream(message, self.encoding) + stream = _make_binary_stream(message, self._encoding) result = self.decrypt_file(stream, **kwargs) stream.close() return result @@ -1349,7 +1349,7 @@ class GPGWrapper(GPG): """Send keys to a keyserver.""" result = self._result_map['list'](self) log.debug('send_keys: %r', keyids) - data = _util._make_binary_stream("", self.encoding) + data = _util._make_binary_stream("", self._encoding) args = ['--keyserver', keyserver, '--send-keys'] args.extend(keyids) self._handle_io(args, data, result, binary=True) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 4435bd3..bf91217 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -554,7 +554,7 @@ class GPGTestCase(unittest.TestCase): def test_signature_string_alternate_encoding(self): key = self.generate_key("Nos Oignons", "nos-oignons.net") - self.gpg.encoding = 'latin-1' + self.gpg._encoding = 'latin-1' message = "Mêle-toi de tes oignons" sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='nosoignons') @@ -603,7 +603,7 @@ class GPGTestCase(unittest.TestCase): passphrase='johanborst') self.assertTrue(sig, "Good passphrase should succeed") try: - file = _util._make_binary_stream(sig.data, self.gpg.encoding) + file = _util._make_binary_stream(sig.data, self.gpg._encoding) verified = self.gpg.verify_file(file) except UnicodeDecodeError: #happens in Python 2.6 verified = self.gpg.verify_file(io.BytesIO(sig.data)) @@ -686,12 +686,12 @@ authentication.""" gentry = str(key.fingerprint) key = self.generate_key("Marten van Dijk", "xorr.ox") dijk = str(key.fingerprint) - self.gpg.encoding = 'latin-1' + self.gpg._encoding = 'latin-1' if _util._py3k: data = 'Hello, André!' else: - data = unicode('Hello, André', self.gpg.encoding) - data = data.encode(self.gpg.encoding) + data = unicode('Hello, André', self.gpg._encoding) + data = data.encode(self.gpg._encoding) encrypted = self.gpg.encrypt(data, gentry) edata = str(encrypted.data) self.assertNotEqual(data, edata) @@ -868,7 +868,7 @@ authentication.""" fdata = enc2.read() ddata = str(self.gpg.decrypt(fdata, passphrase="overalls")) - data = data.encode(self.gpg.encoding) + data = data.encode(self.gpg._encoding) if ddata != data: log.debug("data was: %r" % data) log.debug("new (from filehandle): %r" % fdata) From d910a486607f433efee272b3c38a3dad4b0134cf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 08:52:49 +0000 Subject: [PATCH 022/145] =?UTF-8?q?GPG.filesystemencoding=20=E2=86=92=20GP?= =?UTF-8?q?G.=5Ffilesystemencoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_meta.py b/src/_meta.py index 1a0c7d4..06130f0 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -104,7 +104,7 @@ class GPGBase(object): if encoding is None: # This happens on Jython! encoding = sys.stdin.encoding self._encoding = encoding.lower().replace('-', '_') - self.filesystemencoding = encodings.normalize_encoding( + self._filesystemencoding = encodings.normalize_encoding( sys.getfilesystemencoding().lower()) try: From cacedc6242666dd6f14b65b4398b2e79688e61f9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 09:18:58 +0000 Subject: [PATCH 023/145] =?UTF-8?q?self.gpg=20=E2=86=92=20self.=5Fgpg=20in?= =?UTF-8?q?=20=5Fparsers.py.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_parsers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 8cd8059..38c92cd 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -663,7 +663,7 @@ class GenKey(object): generated key's fingerprint, or a status string explaining the results. """ def __init__(self, gpg): - self.gpg = gpg + self._gpg = gpg ## this should get changed to something more useful, like 'key_type' #: 'P':= primary, 'S':= subkey, 'B':= both self.type = None @@ -713,7 +713,7 @@ class GenKey(object): class DeleteResult(object): """Handle status messages for --delete-keys and --delete-secret-keys""" def __init__(self, gpg): - self.gpg = gpg + self._gpg = gpg self.status = 'ok' def __str__(self): @@ -754,7 +754,7 @@ class Sign(object): what = None def __init__(self, gpg): - self.gpg = gpg + self._gpg = gpg def __nonzero__(self): """Override the determination for truthfulness evaluation. @@ -766,7 +766,7 @@ class Sign(object): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg._encoding, self.gpg._decode_errors) + return self.data.decode(self._gpg._encoding, self._gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. @@ -805,7 +805,7 @@ class ListKeys(list): def __init__(self, gpg): super(ListKeys, self).__init__() - self.gpg = gpg + self._gpg = gpg self.curkey = None self.fingerprints = [] self.uids = [] @@ -867,7 +867,7 @@ class ImportResult(object): results = list() def __init__(self, gpg): - self.gpg = gpg + self._gpg = gpg for result in self.counts: setattr(self, result, None) @@ -999,7 +999,7 @@ class Verify(object): trust_text = None def __init__(self, gpg): - self.gpg = gpg + self._gpg = gpg self.valid = False self.fingerprint = self.creation_date = self.timestamp = None self.signature_id = self.key_id = None @@ -1087,7 +1087,7 @@ class Crypt(Verify): """ def __init__(self, gpg): Verify.__init__(self, gpg) - self.gpg = gpg + self._gpg = gpg self.data = '' self.ok = False self.status = '' @@ -1101,7 +1101,7 @@ class Crypt(Verify): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg._encoding, self.gpg._decode_errors) + return self.data.decode(self._gpg._encoding, self._gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. @@ -1157,7 +1157,7 @@ class ListPackets(object): """ def __init__(self, gpg): - self.gpg = gpg + self._gpg = gpg self.nodata = None self.key = None self.need_passphrase = None From 8e2f905859bf1eff6d66a71a2eb0a5be0f1b84d0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 09:24:06 +0000 Subject: [PATCH 024/145] =?UTF-8?q?handle=5Fstatus()=20=E2=86=92=20=5Fhand?= =?UTF-8?q?le=5Fstatus()=20in=20=5Fparsers.py=20and=20gnupg.py.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_parsers.py | 19 +++++++++---------- src/gnupg.py | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 38c92cd..6235a03 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -686,7 +686,7 @@ class GenKey(object): else: return False - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -723,7 +723,7 @@ class DeleteResult(object): '2': 'Must delete secret key first', '3': 'Ambigious specification', } - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -768,7 +768,7 @@ class Sign(object): def __str__(self): return self.data.decode(self._gpg._encoding, self._gpg._decode_errors) - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -840,7 +840,7 @@ class ListKeys(list): subkey = [args[4], args[11]] self.curkey['subkeys'].append(subkey) - def handle_status(self, key, value): + def _handle_status(self, key, value): pass @@ -896,7 +896,7 @@ class ImportResult(object): '3': 'Certificate Chain too long', '4': 'Error storing certificate', } - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -1020,7 +1020,7 @@ class Verify(object): return self.valid __bool__ = __nonzero__ - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -1103,7 +1103,7 @@ class Crypt(Verify): def __str__(self): return self.data.decode(self._gpg._encoding, self._gpg._decode_errors) - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -1149,7 +1149,7 @@ class Crypt(Verify): ## i.e. '62'→'b':= binary data self.data_format = chr(int(str(fmt), 16)) else: - super(Crypt, self).handle_status(key, value) + super(Crypt, self)._handle_status(key, value) class ListPackets(object): """ @@ -1164,12 +1164,11 @@ class ListPackets(object): self.need_passphrase_sym = None self.userid_hint = None - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. """ - # TODO: write tests for handle_status if key == 'NODATA': self.nodata = True elif key == 'ENC_TO': diff --git a/src/gnupg.py b/src/gnupg.py index d931256..36a6058 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -308,7 +308,7 @@ use_agent: %s value = L[1] else: value = "" - result.handle_status(keyword, value) + result._handle_status(keyword, value) elif line[0:5] == 'gpg: ': log.warn("%s" % line) else: From 6a47c3cd77c9bc5b7e2d872e6ffd8c97f1bf633d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 10:12:01 +0000 Subject: [PATCH 025/145] Move several methods and attributes from GPG to GPGBase. * Move _decode_errors * Move _result_map * Move _copy_data() * Move _read_data() * Move _read_response() * Move _make_args() * Move _open_subprocess() * Move _collect_output() * Move _handle_io() --- src/_meta.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/gnupg.py | 183 +------------------------------------------- 2 files changed, 199 insertions(+), 192 deletions(-) diff --git a/src/_meta.py b/src/_meta.py index 06130f0..bdcd916 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -17,9 +17,13 @@ # GNU Affero General Public License for more details. -from psutil import process_iter +from psutil import process_iter +from subprocess import Popen +from subprocess import PIPE +from threading import Thread import atexit +import codecs import encodings ## For AOS, the locale module will need to point to a wrapper around the ## java.util.Locale class. @@ -28,11 +32,11 @@ import locale import os import sys +import _parsers import _util from _parsers import _check_preferences -from _parsers import _fix_unsafe -from _parsers import _sanitise +from _parsers import _sanitise_list from _util import log from _util import _conf @@ -49,8 +53,8 @@ class GPGMeta(type): """Construct the initialiser for GPG""" log.debug("Metaclass __new__ constructor called for %r" % cls) if cls._find_agent(): - ## call the normal GPG.__init__() initialisor: - attrs['init'] = cls.__init__ ## nothing changed for now + ## call the normal GPG.__init__() initialiser: + attrs['init'] = cls.__init__ attrs['_remove_agent'] = True return super(GPGMeta, cls).__new__(cls, name, bases, attrs) @@ -78,21 +82,31 @@ class GPGMeta(type): class GPGBase(object): - """Base class to control process initialisation and for property storage.""" + """Base class for property storage and controlling process initialisation.""" __metaclass__ = GPGMeta + _decode_errors = 'strict' + _result_map = { 'crypt': _parsers.Crypt, + 'delete': _parsers.DeleteResult, + 'generate': _parsers.GenKey, + 'import': _parsers.ImportResult, + 'list': _parsers.ListKeys, + 'sign': _parsers.Sign, + 'verify': _parsers.Verify, + 'packets': _parsers.ListPackets } + def __init__(self, binary=None, home=None, keyring=None, secring=None, use_agent=False, default_preference_list=None, verbose=False, options=None): self.binary = _util._find_binary(binary) self.homedir = home if home else _conf - pub = _fix_unsafe(keyring) if keyring else 'pubring.gpg' - sec = _fix_unsafe(secring) if secring else 'secring.gpg' + pub = _parsers._fix_unsafe(keyring) if keyring else 'pubring.gpg' + sec = _parsers._fix_unsafe(secring) if secring else 'secring.gpg' self.keyring = os.path.join(self._homedir, pub) self.secring = os.path.join(self._homedir, sec) - self.options = _sanitise(options) if options else None + self.options = _parsers._sanitise(options) if options else None if default_preference_list: self._prefs = _check_preferences(default_preference_list, 'all') @@ -271,7 +285,7 @@ class GPGBase(object): % _conf) directory = _conf - hd = _fix_unsafe(directory) + hd = _parsers._fix_unsafe(directory) log.debug("GPGBase._homedir_setter(): got directory '%s'" % hd) if hd: @@ -292,3 +306,177 @@ class GPGBase(object): self._homedir = hd homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter) + + def _make_args(self, args, passphrase=False): + """Make a list of command line elements for GPG. The value of ``args`` + will be appended only if it passes the checks in + :func:`parsers._sanitise`. The ``passphrase`` argument needs to be True + if a passphrase will be sent to GPG, else False. + + :param list args: A list of strings of options and flags to pass to + ``GPG.binary``. This is input safe, meaning that + these values go through strict checks (see + ``parsers._sanitise_list``) before being passed to to + the input file descriptor for the GnuPG process. + Each string should be given exactly as it would be on + the commandline interface to GnuPG, + e.g. ["--cipher-algo AES256", "--default-key + A3ADB67A2CDB8B35"]. + + :param bool passphrase: If True, the passphrase will be sent to the + stdin file descriptor for the attached GnuPG + process. + """ + ## see TODO file, tag :io:makeargs: + cmd = [self.binary, + '--no-options --no-emit-version --no-tty --status-fd 2'] + + if self.homedir: cmd.append('--homedir "%s"' % self.homedir) + + if self.keyring: + cmd.append('--no-default-keyring --keyring %s' % self.keyring) + if self.secring: + cmd.append('--secret-keyring %s' % self.secring) + + if passphrase: cmd.append('--batch --passphrase-fd 0') + + if self.use_agent: cmd.append('--use-agent') + else: cmd.append('--no-use-agent') + + if self.options: + [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] + if args: + [cmd.append(arg) for arg in iter(_sanitise_list(args))] + + if self.verbose: + cmd.append('--debug-all') + if ((isinstance(self.verbose, str) and + self.verbose in ['basic', 'advanced', 'expert', 'guru']) + or (isinstance(self.verbose, int) and (1<=self.verbose<=9))): + cmd.append('--debug-level %s' % self.verbose) + + return cmd + + def _open_subprocess(self, args=None, passphrase=False): + """Open a pipe to a GPG subprocess and return the file objects for + communicating with it. + + :param list args: A list of strings of options and flags to pass to + ``GPG.binary``. This is input safe, meaning that + these values go through strict checks (see + ``parsers._sanitise_list``) before being passed to to + the input file descriptor for the GnuPG process. + Each string should be given exactly as it would be on + the commandline interface to GnuPG, + e.g. ["--cipher-algo AES256", "--default-key + A3ADB67A2CDB8B35"]. + + :param bool passphrase: If True, the passphrase will be sent to the + stdin file descriptor for the attached GnuPG + process. + """ + ## see http://docs.python.org/2/library/subprocess.html#converting-an\ + ## -argument-sequence-to-a-string-on-windows + cmd = ' '.join(self._make_args(args, passphrase)) + log.debug("Sending command to GnuPG process:%s%s" % (os.linesep, cmd)) + return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) + + def _read_response(self, stream, result): + """Reads all the stderr output from GPG, taking notice only of lines + that begin with the magic [GNUPG:] prefix. + + Calls methods on the response object for each valid token found, with + the arg being the remainder of the status line. + + :param stream: A byte-stream, file handle, or :class:`subprocess.PIPE` + to parse the for status codes from the GnuPG process. + + :param result: The result parser class from :mod:`_parsers` with which + to call ``handle_status`` and parse the output of + ``stream``. + """ + lines = [] + while True: + line = stream.readline() + if len(line) == 0: + break + lines.append(line) + line = line.rstrip() + if line[0:9] == '[GNUPG:] ': + # Chop off the prefix + line = line[9:] + log.status("%s" % line) + L = line.split(None, 1) + keyword = L[0] + if len(L) > 1: + value = L[1] + else: + value = "" + result._handle_status(keyword, value) + elif line[0:5] == 'gpg: ': + log.warn("%s" % line) + else: + if self.verbose: + log.info("%s" % line) + else: + log.debug("%s" % line) + result.stderr = ''.join(lines) + + def _read_data(self, stream, result): + """Read the contents of the file from GPG's stdout.""" + chunks = [] + while True: + data = stream.read(1024) + if len(data) == 0: + break + log.debug("read from stdout: %r" % data[:256]) + chunks.append(data) + if _util._py3k: + # Join using b'' or '', as appropriate + result.data = type(data)().join(chunks) + else: + result.data = ''.join(chunks) + + def _collect_output(self, process, result, writer=None, stdin=None): + """Drain the subprocesses output streams, writing the collected output + to the result. If a writer thread (writing to the subprocess) is given, + make sure it's joined before returning. If a stdin stream is given, + close it before returning. + """ + stderr = codecs.getreader(self._encoding)(process.stderr) + rr = Thread(target=self._read_response, args=(stderr, result)) + rr.setDaemon(True) + log.debug('stderr reader: %r', rr) + rr.start() + + stdout = process.stdout + dr = Thread(target=self._read_data, args=(stdout, result)) + dr.setDaemon(True) + log.debug('stdout reader: %r', dr) + dr.start() + + dr.join() + rr.join() + if writer is not None: + writer.join() + process.wait() + if stdin is not None: + try: + stdin.close() + except IOError: + pass + stderr.close() + stdout.close() + + def _handle_io(self, args, file, result, passphrase=False, binary=False): + """Handle a call to GPG - pass input data, collect output data.""" + p = self._open_subprocess(args, passphrase) + if not binary: + stdin = codecs.getwriter(self._encoding)(p.stdin) + else: + stdin = p.stdin + if passphrase: + _util._write_passphrase(stdin, passphrase, self._encoding) + writer = _util._threaded_copy_data(file, stdin) + self._collect_output(p, result, writer, stdin) + return result diff --git a/src/gnupg.py b/src/gnupg.py index 36a6058..0990bc4 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -78,21 +78,16 @@ try: except ImportError: from cStringIO import StringIO -from codecs import open as open -from subprocess import Popen -from subprocess import PIPE +from codecs import open as open -import codecs import encodings import os -import threading import _parsers import _util from _meta import GPGBase from _parsers import _fix_unsafe -from _parsers import _sanitise_list from _util import _is_list_or_tuple from _util import _is_stream from _util import _make_binary_stream @@ -102,15 +97,6 @@ from _util import log class GPG(GPGBase): """Encapsulate access to the gpg executable""" - _decode_errors = 'strict' - _result_map = { 'crypt': _parsers.Crypt, - 'delete': _parsers.DeleteResult, - 'generate': _parsers.GenKey, - 'import': _parsers.ImportResult, - 'list': _parsers.ListKeys, - 'sign': _parsers.Sign, - 'verify': _parsers.Verify, - 'packets': _parsers.ListPackets } #: The number of simultaneous keyids we should list operations like #: '--list-sigs' to: _batch_limit = 25 @@ -210,173 +196,6 @@ use_agent: %s raise RuntimeError("Error invoking gpg: %s: %s" % (proc.returncode, result.stderr)) - def _make_args(self, args, passphrase=False): - """Make a list of command line elements for GPG. The value of ``args`` - will be appended only if it passes the checks in - :func:`parsers._sanitise`. The ``passphrase`` argument needs to be True - if a passphrase will be sent to GPG, else False. - - :param list args: A list of strings of options and flags to pass to - ``GPG.binary``. This is input safe, meaning that - these values go through strict checks (see - ``parsers._sanitise_list``) before being passed to to - the input file descriptor for the GnuPG process. - Each string should be given exactly as it would be on - the commandline interface to GnuPG, - e.g. ["--cipher-algo AES256", "--default-key - A3ADB67A2CDB8B35"]. - - :param bool passphrase: If True, the passphrase will be sent to the - stdin file descriptor for the attached GnuPG - process. - """ - ## see TODO file, tag :io:makeargs: - cmd = [self.binary, - '--no-options --no-emit-version --no-tty --status-fd 2'] - - if self.homedir: cmd.append('--homedir "%s"' % self.homedir) - - if self.keyring: - cmd.append('--no-default-keyring --keyring %s' % self.keyring) - if self.secring: - cmd.append('--secret-keyring %s' % self.secring) - - if passphrase: cmd.append('--batch --passphrase-fd 0') - - if self.use_agent: cmd.append('--use-agent') - else: cmd.append('--no-use-agent') - - if self.options: - [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] - if args: - [cmd.append(arg) for arg in iter(_sanitise_list(args))] - - if self.verbose: - cmd.append('--debug-all') - if ((isinstance(self.verbose, str) and - self.verbose in ['basic', 'advanced', 'expert', 'guru']) - or (isinstance(self.verbose, int) and (1<=self.verbose<=9))): - cmd.append('--debug-level %s' % self.verbose) - - return cmd - - def _open_subprocess(self, args=None, passphrase=False): - """Open a pipe to a GPG subprocess and return the file objects for - communicating with it. - - :param list args: A list of strings of options and flags to pass to - ``GPG.binary``. This is input safe, meaning that - these values go through strict checks (see - ``parsers._sanitise_list``) before being passed to to - the input file descriptor for the GnuPG process. - Each string should be given exactly as it would be on - the commandline interface to GnuPG, - e.g. ["--cipher-algo AES256", "--default-key - A3ADB67A2CDB8B35"]. - - :param bool passphrase: If True, the passphrase will be sent to the - stdin file descriptor for the attached GnuPG - process. - """ - ## see http://docs.python.org/2/library/subprocess.html#converting-an\ - ## -argument-sequence-to-a-string-on-windows - cmd = ' '.join(self._make_args(args, passphrase)) - log.debug("Sending command to GnuPG process:%s%s" % (os.linesep, cmd)) - return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) - - def _read_response(self, stream, result): - """Reads all the stderr output from GPG, taking notice only of lines - that begin with the magic [GNUPG:] prefix. - - Calls methods on the response object for each valid token found, with - the arg being the remainder of the status line. - """ - lines = [] - while True: - line = stream.readline() - if len(line) == 0: - break - lines.append(line) - line = line.rstrip() - if line[0:9] == '[GNUPG:] ': - # Chop off the prefix - line = line[9:] - log.status("%s" % line) - L = line.split(None, 1) - keyword = L[0] - if len(L) > 1: - value = L[1] - else: - value = "" - result._handle_status(keyword, value) - elif line[0:5] == 'gpg: ': - log.warn("%s" % line) - else: - if self.verbose: - log.info("%s" % line) - else: - log.debug("%s" % line) - result.stderr = ''.join(lines) - - def _read_data(self, stream, result): - """Read the contents of the file from GPG's stdout.""" - chunks = [] - while True: - data = stream.read(1024) - if len(data) == 0: - break - log.debug("read from stdout: %r" % data[:256]) - chunks.append(data) - if _util._py3k: - # Join using b'' or '', as appropriate - result.data = type(data)().join(chunks) - else: - result.data = ''.join(chunks) - - def _collect_output(self, process, result, writer=None, stdin=None): - """Drain the subprocesses output streams, writing the collected output - to the result. If a writer thread (writing to the subprocess) is given, - make sure it's joined before returning. If a stdin stream is given, - close it before returning. - """ - stderr = codecs.getreader(self._encoding)(process.stderr) - rr = threading.Thread(target=self._read_response, args=(stderr, result)) - rr.setDaemon(True) - log.debug('stderr reader: %r', rr) - rr.start() - - stdout = process.stdout - dr = threading.Thread(target=self._read_data, args=(stdout, result)) - dr.setDaemon(True) - log.debug('stdout reader: %r', dr) - dr.start() - - dr.join() - rr.join() - if writer is not None: - writer.join() - process.wait() - if stdin is not None: - try: - stdin.close() - except IOError: - pass - stderr.close() - stdout.close() - - def _handle_io(self, args, file, result, passphrase=False, binary=False): - """Handle a call to GPG - pass input data, collect output data.""" - p = self._open_subprocess(args, passphrase) - if not binary: - stdin = codecs.getwriter(self._encoding)(p.stdin) - else: - stdin = p.stdin - if passphrase: - _util._write_passphrase(stdin, passphrase, self._encoding) - writer = _util._threaded_copy_data(file, stdin) - self._collect_output(p, result, writer, stdin) - return result - def sign(self, data, **kwargs): """Create a signature for a message string or file. From 7129a4c85f13628d7a1244f6cc998ffaf1c389ee Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 10:15:51 +0000 Subject: [PATCH 026/145] Update docstring for GPGBase._find_agent(). --- src/_meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_meta.py b/src/_meta.py index bdcd916..dd2f351 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -62,9 +62,9 @@ class GPGMeta(type): def _find_agent(cls): """Discover if a gpg-agent process for the current euid is running. - If there is a matching gpg-agent process, set a :class:psutil.Process + If there is a matching gpg-agent process, set a :class:`psutil.Process` instance containing the gpg-agent process' information to - :attr:cls._agent_proc. + :attr:`GPG._agent_proc`. :returns: True if there exists a gpg-agent process running under the same effective user ID as that of this program. Otherwise, From b7ba572aa161efd4eb67e2cf216ecc700aaca003 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 10:16:16 +0000 Subject: [PATCH 027/145] Update docstring for GPGBase.__remove_path__(). --- src/_meta.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/_meta.py b/src/_meta.py index dd2f351..8c8e5c5 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -142,11 +142,16 @@ class GPGBase(object): def __remove_path__(self, prog=None, at_exit=True): """Remove a the directories containing a program from the system's - $PATH. If self.gpg.binary is in a directory being removed, it is - symlinked to './gpg' + ``$PATH``. If :attr:`GPG.binary` is in a directory being removed, it + is symlinked to './gpg' - :param str prog: + :param str prog: The program to remove from ``$PATH``. + + :param bool at_exit: Add the program back into the ``$PATH`` when the + Python interpreter exits, and delete any symlinks + to :attr:`GPG.binary` which were created. """ + #: A list of ``$PATH`` entries which were removed to disable pinentry. self._removed_path_entries = [] log.debug("Attempting to remove %s from system PATH" % str(prog)) @@ -181,9 +186,12 @@ class GPGBase(object): """Remove all directories which contain a program from PATH. :param str path: The contents of the system environment's - PATH. - :param str prog_base: The base (directory only) portion of a - program's location. + ``$PATH``. + + :param str prog_base: The directory portion of a program's + location, without the trailing slash, + and without the program name. For + example, ``prog_base='/usr/bin'``. """ paths = path.split(':') for directory in paths: From 15d4bc0eb4ce23c6ef438068aae789aedb4ca56d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 10:17:43 +0000 Subject: [PATCH 028/145] Fix a bug resulting from a typo in the @atexit.register'd function. --- src/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_meta.py b/src/_meta.py index 8c8e5c5..274a8f7 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -231,7 +231,7 @@ class GPGBase(object): def remove_symlinked_binary(): loc = os.path.join(os.getcwd(), 'gpg') if os.path.islink(loc): - os.unline(loc) + os.unlink(loc) log.debug("Removed binary symlink '%s'" % loc) @property From 4bd45d06cd262ab70ce0a2c517f36a78f2528482 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 10:18:43 +0000 Subject: [PATCH 029/145] Update docstring for GPGBase._homedir_getter() property. --- src/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_meta.py b/src/_meta.py index 274a8f7..8bfb682 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -266,7 +266,7 @@ class GPGBase(object): def _homedir_getter(self): """Get the directory currently being used as GnuPG's homedir. - If unspecified, use $HOME/.config/python-gnupg/ + If unspecified, use :file:`~/.config/python-gnupg/` :rtype: str :returns: The absolute path to the current GnuPG homedir. From 694ae87410abce710e535725d68f515773aa45df Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 11:00:13 +0000 Subject: [PATCH 030/145] =?UTF-8?q?sign=5Fwith=20=E2=86=92=20default=5Fkey?= =?UTF-8?q?.=20Also,=20deduplicate=20code.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 0990bc4..95b9c5c 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -224,26 +224,18 @@ use_agent: %s :param bool detach: If True, create a detached signature. :param bool binary: If True, do not ascii armour the output. """ - if isinstance(data, file): - log.warn("Note: This function is not for signing other keys,") - log.warn(" see the docstring for GPG.sign()") - if 'keyid' in kwargs.items(): - log.info("Signing file '%r' with keyid: %s" - % (data, kwargs[keyid])) - else: - log.warn("No 'sign_with' keyid given! Using default key.") - result = self._sign_file(data, **kwargs) + if 'default_key' in kwargs.items(): + log.info("Signing message '%r' with keyid: %s" + % (data, kwargs['default_key'])) + else: + log.warn("No 'default_key' given! Using first key on secring.") + if isinstance(data, file): + result = self._sign_file(data, **kwargs) elif not _is_stream(data): - if 'keyid' in kwargs.items(): - log.info("Signing data string '%s' with keyid: %s" - % (data, kwargs[keyid])) - else: - log.warn("No 'sign_with' keyid given! Using default key.") stream = _make_binary_stream(data, self._encoding) result = self._sign_file(stream, **kwargs) stream.close() - else: log.warn("Unable to sign message '%s' with type %s" % (data, type(data))) From a5c0f8f1cf83fc57495df52f7775eef079f466e2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 11:01:13 +0000 Subject: [PATCH 031/145] Add setuptools directive to include PKG-INFO. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7135b6d..198a1cf 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ It is intended for use with Python 2.6 or greater.", url="https://github.com/isislovecruft/python-gnupg", package_dir={'gnupg': 'src'}, packages=['gnupg'], + include_package_data=True, platforms="Linux, BSD, OSX, Windows", download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", classifiers=[ @@ -45,4 +46,4 @@ It is intended for use with Python 2.6 or greater.", 'Classifier:: Topic :: Security :: Cryptography', 'Classifier:: Topic :: Software Development :: Libraries :: Python Modules', 'Classifier:: Topic :: Utilities',] - ) + ) From 158499da3585ceacd2acc4ae4fbddb3e3427fbc0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 11:04:52 +0000 Subject: [PATCH 032/145] Add setuptools-git as a dependency. * TODO find a way to only require setuptools-git if python-gnupg was checked out from a git repo. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c82f9d5..9cef333 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Sphinx>=1.1 psutil>=0.5.1 +setuptools-git>1.0b1 From 0c0f0c069a82c42e782f6dec7e88fb62bc20858d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 14 May 2013 11:08:18 +0000 Subject: [PATCH 033/145] Move LICENSE file to docs/ subfolder. --- LICENSE | 656 +-------------------------------------------------- docs/LICENSE | 655 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 656 insertions(+), 655 deletions(-) mode change 100644 => 120000 LICENSE create mode 100644 docs/LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c02cef5..0000000 --- a/LICENSE +++ /dev/null @@ -1,655 +0,0 @@ -Copyright (c) 2013 Isis Lovecruft -Copyright (c) 2008-2012 by Vinay Sajip. -All rights reserved. - - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - - - BEGIN ORIGINAL LICENSE TEXT - -Copyright (c) 2008-2012 by Vinay Sajip. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * The name(s) of the copyright holder(s) may not be used to endorse or - promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - END ORIGINAL LICENSE TEXT \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..2374c87 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +docs/LICENSE \ No newline at end of file diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 0000000..c02cef5 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,655 @@ +Copyright (c) 2013 Isis Lovecruft +Copyright (c) 2008-2012 by Vinay Sajip. +All rights reserved. + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + + + BEGIN ORIGINAL LICENSE TEXT + +Copyright (c) 2008-2012 by Vinay Sajip. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name(s) of the copyright holder(s) may not be used to endorse or + promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + END ORIGINAL LICENSE TEXT \ No newline at end of file From 0462e8509cf75f9465aec17c59d9a86b67bc2113 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 14 May 2013 11:10:45 +0000 Subject: [PATCH 034/145] Move TODO file to docs/ subfolder. --- TODO | 108 +----------------------------------------------------- docs/TODO | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 107 deletions(-) mode change 100644 => 120000 TODO create mode 100644 docs/TODO diff --git a/TODO b/TODO deleted file mode 100644 index 58e6520..0000000 --- a/TODO +++ /dev/null @@ -1,107 +0,0 @@ --*- mode: org -*- - -* Keyring separation :keyseparation: -** TODO in GPG.gen_key() :keyseparation:gen_key: -It would be nice to have an option for gen_key() [[gnupg.py:927]] to -automatically switch before key generation to a new tempfile.mkdtemp() -directory, with a new keyring and secring, and then to rename either the -directory or the keyrings with the long keyid of the key which was freshly -generated. - -* I/O :io: -** TODO in GPG.__make_args() :io:makeargs: -It would be nice to make the file descriptors for communication with the GnuPG -process configurable, and not the default, hard-coded 0=stdin 1=stdout -2=stderr. - -* Key editing :editkey: -** TODO add '--edit-key' feature :editkey: -see :compatibility:gen__key_input: - -* Compatibility between GnuPG versions :compatibility: -** TODO GnuPG>=2.1.0 won't allow key generation with preset passphrase -*** TODO in GPG.gen__key_input() :compatibility:gen_key_input: -In the docstring of GPG.gen__key_input() [[gnupg.py:1068]], for the parameter -'passphrase', it is explained that: - - :param str passphrase: The passphrase for the new key. The default is - to not use any passphrase. Note that - GnuPG>=2.1.x will not allow you to specify a - passphrase for batch key generation -- GnuPG - will ignore the ``passphrase`` parameter, stop, - and ask the user for the new passphrase. - However, we can put the command '%no-protection' - into the batch key generation file to allow a - passwordless key to be created, which can then - have its passphrase set later with '--edit-key'. - -If we add a GnuPG version detection feature (the version string is already -obtained in GPG.___init___() [[gnupg.py:407]]), then we can automatically chain -GPG.gen__key_input() to another new feature for '--edit-key'. This chaining -would likely need to happen here [[gnupg.py:1146]]. - -*** TODO add '--edit-key' feature :editkey: -This would be necessary for adding a passphrase to the key after passwordless -generation in GnuPG>=2.1.0. - -** TODO GnuPG==1.4.12 doesn't process "Key-Type: default" in batch files -(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ ipython -WARNING: Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. - -In [1]: import gnupg -GnuPG logging disabled... - -In [2]: gpg = gnupg.GPG(homedir='./tests/doctests') - -In [3]: key_input = gpg.gen_key_input() - -In [4]: print key_input -Key-Type: default -Key-Length: 4096 -Subkey-Type: default -Name-Email: isis@wintermute -Expire-Date: 2014-05-28 -Name-Real: Autogenerated Key -%commit - - -In [5]: key = gpg.gen_key(key_input) - -In [6]: print key.stderr -gpg: -:1: invalid algorithm -[GNUPG:] KEY_NOT_CREATED - - -In [7]: quit() -(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ which gpg -/usr/bin/gpg -(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ gpg --version -gpg (GnuPG) 1.4.12 -Copyright (C) 2012 Free Software Foundation, Inc. -License GPLv3+: GNU GPL version 3 or later -This is free software: you are free to change and redistribute it. -There is NO WARRANTY, to the extent permitted by law. - -Home: ~/.gnupg -Supported algorithms: -Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA -Cipher: 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, - CAMELLIA192, CAMELLIA256 -Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 -Compression: Uncompressed, ZIP, ZLIB, BZIP2 -(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ - -* Code cleanup :cleanup: -** TODO in parsers.__sanitise() :cleanup:sanitise: -Ughh...this is the ugliest code I think I've ever written. It works, but I -worry that it is fragile, not to mention *I* have trouble reading it, and I -fucking wrote the damn thing. There's probably not much that could be done to -make it more Pythonic, because type checks and input validation are pretty much -intrinsically non-Pythonic. But did i mention that it's ugly? I'm sure these -functions would be pretty glad to get a shower, shave, and haircut. - -** TODO in parsers.__is_allowed() :cleanup:is_allowed: -There is a lot of madness dealing with stupid things like hyphens -vs. underscores, and lists of options vs. strings. This can *definitely* be -cleaned up. - diff --git a/TODO b/TODO new file mode 120000 index 0000000..5cf7353 --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +docs/TODO \ No newline at end of file diff --git a/docs/TODO b/docs/TODO new file mode 100644 index 0000000..58e6520 --- /dev/null +++ b/docs/TODO @@ -0,0 +1,107 @@ +-*- mode: org -*- + +* Keyring separation :keyseparation: +** TODO in GPG.gen_key() :keyseparation:gen_key: +It would be nice to have an option for gen_key() [[gnupg.py:927]] to +automatically switch before key generation to a new tempfile.mkdtemp() +directory, with a new keyring and secring, and then to rename either the +directory or the keyrings with the long keyid of the key which was freshly +generated. + +* I/O :io: +** TODO in GPG.__make_args() :io:makeargs: +It would be nice to make the file descriptors for communication with the GnuPG +process configurable, and not the default, hard-coded 0=stdin 1=stdout +2=stderr. + +* Key editing :editkey: +** TODO add '--edit-key' feature :editkey: +see :compatibility:gen__key_input: + +* Compatibility between GnuPG versions :compatibility: +** TODO GnuPG>=2.1.0 won't allow key generation with preset passphrase +*** TODO in GPG.gen__key_input() :compatibility:gen_key_input: +In the docstring of GPG.gen__key_input() [[gnupg.py:1068]], for the parameter +'passphrase', it is explained that: + + :param str passphrase: The passphrase for the new key. The default is + to not use any passphrase. Note that + GnuPG>=2.1.x will not allow you to specify a + passphrase for batch key generation -- GnuPG + will ignore the ``passphrase`` parameter, stop, + and ask the user for the new passphrase. + However, we can put the command '%no-protection' + into the batch key generation file to allow a + passwordless key to be created, which can then + have its passphrase set later with '--edit-key'. + +If we add a GnuPG version detection feature (the version string is already +obtained in GPG.___init___() [[gnupg.py:407]]), then we can automatically chain +GPG.gen__key_input() to another new feature for '--edit-key'. This chaining +would likely need to happen here [[gnupg.py:1146]]. + +*** TODO add '--edit-key' feature :editkey: +This would be necessary for adding a passphrase to the key after passwordless +generation in GnuPG>=2.1.0. + +** TODO GnuPG==1.4.12 doesn't process "Key-Type: default" in batch files +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ ipython +WARNING: Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv. + +In [1]: import gnupg +GnuPG logging disabled... + +In [2]: gpg = gnupg.GPG(homedir='./tests/doctests') + +In [3]: key_input = gpg.gen_key_input() + +In [4]: print key_input +Key-Type: default +Key-Length: 4096 +Subkey-Type: default +Name-Email: isis@wintermute +Expire-Date: 2014-05-28 +Name-Real: Autogenerated Key +%commit + + +In [5]: key = gpg.gen_key(key_input) + +In [6]: print key.stderr +gpg: -:1: invalid algorithm +[GNUPG:] KEY_NOT_CREATED + + +In [7]: quit() +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ which gpg +/usr/bin/gpg +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ gpg --version +gpg (GnuPG) 1.4.12 +Copyright (C) 2012 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Home: ~/.gnupg +Supported algorithms: +Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA +Cipher: 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, + CAMELLIA192, CAMELLIA256 +Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 +Compression: Uncompressed, ZIP, ZLIB, BZIP2 +(python-gnupg)∃!isisⒶwintermute:~/code/riseup/python-gnupg ∴ + +* Code cleanup :cleanup: +** TODO in parsers.__sanitise() :cleanup:sanitise: +Ughh...this is the ugliest code I think I've ever written. It works, but I +worry that it is fragile, not to mention *I* have trouble reading it, and I +fucking wrote the damn thing. There's probably not much that could be done to +make it more Pythonic, because type checks and input validation are pretty much +intrinsically non-Pythonic. But did i mention that it's ugly? I'm sure these +functions would be pretty glad to get a shower, shave, and haircut. + +** TODO in parsers.__is_allowed() :cleanup:is_allowed: +There is a lot of madness dealing with stupid things like hyphens +vs. underscores, and lists of options vs. strings. This can *definitely* be +cleaned up. + From df0b4c7a34f39bab8b8f9a15c5864327c8b1054c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 7 May 2013 14:23:34 +0000 Subject: [PATCH 035/145] Add pip installation update script to make sure we use SSL for downloads. --- scripts/get-pip.py | 1153 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1153 insertions(+) create mode 100755 scripts/get-pip.py diff --git a/scripts/get-pip.py b/scripts/get-pip.py new file mode 100755 index 0000000..9fd5b30 --- /dev/null +++ b/scripts/get-pip.py @@ -0,0 +1,1153 @@ +#! /usr/bin/env python + +sources = """ +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 +""" + +import os +import sys +import base64 +import zlib +import tempfile +import shutil + + +def unpack(sources): + temp_dir = tempfile.mkdtemp('-scratchdir', 'unpacker-') + for package, content in sources.items(): + filepath = package.split(".") + dirpath = os.sep.join(filepath[:-1]) + packagedir = os.path.join(temp_dir, dirpath) + if not os.path.isdir(packagedir): + os.makedirs(packagedir) + mod = open(os.path.join(packagedir, "%s.py" % filepath[-1]), 'wb') + try: + mod.write(content.encode("ascii")) + finally: + mod.close() + return temp_dir + +if __name__ == "__main__": + if sys.version_info >= (3, 0): + exec("def do_exec(co, loc): exec(co, loc)\n") + import pickle + sources = sources.encode("ascii") # ensure bytes + sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) + else: + import cPickle as pickle + exec("def do_exec(co, loc): exec co in loc\n") + sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) + + try: + temp_dir = unpack(sources) + sys.path.insert(0, temp_dir) + + entry = """ +import sys +try: + import setuptools + import pkg_resources +except ImportError: + raise SystemExit("An error occured while trying to run %s. Make sure " + "you have setuptools or distribute installed." % __file__) +import pip +pip.bootstrap() +""" + do_exec(entry, locals()) + finally: + shutil.rmtree(temp_dir) From 2114033c3acae0b2c80f40b36df2792a4d71659b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 7 May 2013 15:01:22 +0000 Subject: [PATCH 036/145] Add distribute_setup.py script. --- scripts/distribute_setup.py | 558 ++++++++++++++++++++++++++++++++++++ 1 file changed, 558 insertions(+) create mode 100755 scripts/distribute_setup.py diff --git a/scripts/distribute_setup.py b/scripts/distribute_setup.py new file mode 100755 index 0000000..dc461f3 --- /dev/null +++ b/scripts/distribute_setup.py @@ -0,0 +1,558 @@ +#!python +"""Bootstrap distribute installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from distribute_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. + +This file was taken from http://nightly.ziade.org/distribute_setup.py +on 2013-05-27. +""" +import os +import shutil +import sys +import time +import fnmatch +import tempfile +import tarfile +import optparse + +from distutils import log + +try: + from site import USER_SITE +except ImportError: + USER_SITE = None + +try: + import subprocess + + def _python_cmd(*args): + args = (sys.executable,) + args + return subprocess.call(args) == 0 + +except ImportError: + # will be used for python 2.3 + def _python_cmd(*args): + args = (sys.executable,) + args + # quoting arguments if windows + if sys.platform == 'win32': + def quote(arg): + if ' ' in arg: + return '"%s"' % arg + return arg + args = [quote(arg) for arg in args] + return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 + +DEFAULT_VERSION = "0.6.44" +DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" +SETUPTOOLS_FAKED_VERSION = "0.6c11" + +SETUPTOOLS_PKG_INFO = """\ +Metadata-Version: 1.0 +Name: setuptools +Version: %s +Summary: xxxx +Home-page: xxx +Author: xxx +Author-email: xxx +License: xxx +Description: xxx +""" % SETUPTOOLS_FAKED_VERSION + + +def _install(tarball, install_args=()): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # installing + log.warn('Installing Distribute') + if not _python_cmd('setup.py', 'install', *install_args): + log.warn('Something went wrong during the installation.') + log.warn('See the error message above.') + # exitcode will be 2 + return 2 + finally: + os.chdir(old_wd) + shutil.rmtree(tmpdir) + + +def _build_egg(egg, tarball, to_dir): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # building an egg + log.warn('Building a Distribute egg in %s', to_dir) + _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + + finally: + os.chdir(old_wd) + shutil.rmtree(tmpdir) + # returning the result + log.warn(egg) + if not os.path.exists(egg): + raise IOError('Could not build the egg.') + + +def _do_download(version, download_base, to_dir, download_delay): + egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' + % (version, sys.version_info[0], sys.version_info[1])) + if not os.path.exists(egg): + tarball = download_setuptools(version, download_base, + to_dir, download_delay) + _build_egg(egg, tarball, to_dir) + sys.path.insert(0, egg) + import setuptools + setuptools.bootstrap_install_from = egg + + +def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, download_delay=15, no_fake=True): + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + was_imported = 'pkg_resources' in sys.modules or \ + 'setuptools' in sys.modules + try: + try: + import pkg_resources + + # Setuptools 0.7b and later is a suitable (and preferable) + # substitute for any Distribute version. + try: + pkg_resources.require("setuptools>=0.7b") + return + except pkg_resources.DistributionNotFound: + pass + + if not hasattr(pkg_resources, '_distribute'): + if not no_fake: + _fake_setuptools() + raise ImportError + except ImportError: + return _do_download(version, download_base, to_dir, download_delay) + try: + pkg_resources.require("distribute>=" + version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + if not hasattr(DirectorySandbox, '_old'): + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + else: + patched = False + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + + +def _patch_file(path, content): + """Will backup the file then patch it""" + f = open(path) + existing_content = f.read() + f.close() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + +_patch_file = _no_sandbox(_patch_file) + + +def _same_content(path, content): + f = open(path) + existing_content = f.read() + f.close() + return existing_content == content + + +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s to %s', path, new_name) + os.rename(path, new_name) + return new_name + + +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Moving elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + +_remove_flat_installation = _no_sandbox(_remove_flat_installation) + + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + + +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + try: + f = open(pkg_info, 'w') + except EnvironmentError: + log.warn("Don't have permissions to write %s, skipping", pkg_info) + return + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +_create_fake_setuptools_pkg_info = _no_sandbox( + _create_fake_setuptools_pkg_info +) + + +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + +_patch_egg_dir = _no_sandbox(_patch_egg_dir) + + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install') + 1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index + 1] + return location.startswith(top_dir) + if arg == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find( + pkg_resources.Requirement.parse('setuptools', replacement=False) + ) + except TypeError: + # old distribute API + setuptools_dist = ws.find( + pkg_resources.Requirement.parse('setuptools') + ) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patching complete.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + # pip marker to avoid a relaunch bug + _cmd1 = ['-c', 'install', '--single-version-externally-managed'] + _cmd2 = ['-c', 'install', '--record'] + if sys.argv[:3] == _cmd1 or sys.argv[:3] == _cmd2: + sys.argv[0] = 'setup.py' + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def _build_install_args(options): + """ + Build the arguments to 'python setup.py install' on the distribute package + """ + install_args = [] + if options.user_install: + if sys.version_info < (2, 6): + log.warn("--user requires Python 2.6 or later") + raise SystemExit(1) + install_args.append('--user') + return install_args + +def _parse_args(): + """ + Parse the command line for options + """ + parser = optparse.OptionParser() + parser.add_option( + '--user', dest='user_install', action='store_true', default=False, + help='install in user site package (requires Python 2.6 or later)') + parser.add_option( + '--download-base', dest='download_base', metavar="URL", + default=DEFAULT_URL, + help='alternative URL from where to download the distribute package') + options, args = parser.parse_args() + # positional arguments are ignored + return options + +def main(version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + options = _parse_args() + tarball = download_setuptools(download_base=options.download_base) + return _install(tarball, _build_install_args(options)) + +if __name__ == '__main__': + sys.exit(main()) From 0d1358ee94fdab7ed415724ee812526884c30b8e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 20 May 2013 09:30:20 +0000 Subject: [PATCH 037/145] Add headers to tests/test_gnupg.py. --- tests/test_gnupg.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index bf91217..5b24856 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -1,11 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +"""test_gnupg.py +---------------- +A test harness and unittests for gnupg.py. """ -A test harness for gnupg.py. -Copyright © 2013 Isis Lovecruft. -Copyright © 2008-2013 Vinay Sajip. All rights reserved. -""" from functools import wraps From a34084c33239e96f0d29583c3db65c296181b27c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 20 May 2013 09:30:20 +0000 Subject: [PATCH 038/145] Use py3k syntax for relative imports in test/test_gnupg.py. --- tests/test_gnupg.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 5b24856..96d57d1 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -21,6 +21,10 @@ A test harness and unittests for gnupg.py. """ +from __future__ import absolute_import +from __future__ import print_function +from time import gmtime +from time import mktime from functools import wraps @@ -34,7 +38,6 @@ import os import shutil import sys import tempfile -import time ## Use unittest2 if we're on Python2.6 or less: if sys.version_info.major == 2 and sys.version_info.minor <= 6: @@ -42,11 +45,12 @@ if sys.version_info.major == 2 and sys.version_info.minor <= 6: else: import unittest -import gnupg -from gnupg import _parsers -from gnupg import _util -from gnupg import _logger +## see PEP-328 http://docs.python.org/2.5/whatsnew/pep-328.html +import .gnupg +import ._parsers +import ._util +import ._logger log = _util.log log.setLevel(9) @@ -153,7 +157,7 @@ class GPGTestCase(unittest.TestCase): def setUp(self): """This method is called once per self.test_* method.""" - print "%s%s%s" % (os.linesep, str("=" * 70), os.linesep) + print("%s%s%s" % (os.linesep, str("=" * 70), os.linesep)) hd = tempfile.mkdtemp() if os.path.exists(hd): if not RETAIN_TEST_DIRS: @@ -186,7 +190,7 @@ class GPGTestCase(unittest.TestCase): """Test that unsafe inputs are quoted out and then ignored.""" shell_input = "\"&coproc /bin/sh\"" fixed = _parsers._fix_unsafe(shell_input) - print fixed + print(fixed) test_file = os.path.join(_files, 'cypherpunk_manifesto') self.assertTrue(os.path.isfile(test_file)) has_shell = self.gpg.verify_file(test_file, fixed) @@ -276,7 +280,7 @@ class GPGTestCase(unittest.TestCase): def test_gpg_binary_not_abs(self): """Test that a non-absolute path to gpg results in a full path.""" - print self.gpg.binary + print(self.gpg.binary) self.assertTrue(os.path.isabs(self.gpg.binary)) def test_make_args_drop_protected_options(self): @@ -355,8 +359,8 @@ class GPGTestCase(unittest.TestCase): """Generate a basic key.""" key_input = self.generate_key_input(real_name, email_domain, **kwargs) key = self.gpg.gen_key(key_input) - print "\nKEY TYPE: ", key.type - print "KEY FINGERPRINT: ", key.fingerprint + print("\nKEY TYPE: ", key.type) + print("KEY FINGERPRINT: ", key.fingerprint) return key def test_gen_key_input(self): @@ -547,7 +551,7 @@ class GPGTestCase(unittest.TestCase): message = "Damn, I really wish GnuPG had ECC support." sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='wernerkoch') - print "SIGNATURE:\n", sig.data + print("SIGNATURE:\n", sig.data) self.assertIsNotNone(sig.data) def test_signature_algorithm(self): @@ -556,7 +560,7 @@ class GPGTestCase(unittest.TestCase): message = "Someone should add GCM block cipher mode to PyCrypto." sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='ronrivest') - print "ALGORITHM:\n", sig.sig_algo + print("ALGORITHM:\n", sig.sig_algo) self.assertIsNotNone(sig.sig_algo) def test_signature_string_bad_passphrase(self): @@ -591,7 +595,7 @@ class GPGTestCase(unittest.TestCase): message += '[hackers in popular culture] to push for more power' sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='bruceschneier') - now = time.mktime(time.gmtime()) + now = mktime(gmtime()) self.assertTrue(sig, "Good passphrase should succeed") verified = self.gpg.verify(sig.data) self.assertIsNotNone(verified.fingerprint) @@ -659,7 +663,7 @@ class GPGTestCase(unittest.TestCase): detach=True, binary=True, clearsign=False) self.assertTrue(sig.data, "File signing should succeed") with self.assertRaises(UnicodeDecodeError): - print "SIG=", sig + print("SIG=%s" % sig) def test_deletion(self): """Test that key deletion works.""" @@ -714,7 +718,7 @@ authentication.""" def test_encryption_multi_recipient(self): """Test encrypting a message for multiple recipients""" - self.gpg.homedir = _here + self.gpg.homedir = _util._here ian = { 'name_real': 'Ian Goldberg', 'name_email': 'gold@stein', From 780360ea9f4cfcd4904628bbf4a3ebb8a6b969b0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 24 May 2013 19:32:33 +0000 Subject: [PATCH 039/145] Add missing test keyring file. --- tests/files/kat.pub | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/files/kat.pub diff --git a/tests/files/kat.pub b/tests/files/kat.pub new file mode 100644 index 0000000..34a45ae --- /dev/null +++ b/tests/files/kat.pub @@ -0,0 +1,47 @@ + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +04PY9ENrTNnjCWTKcw== +=hLUv +-----END PGP PUBLIC KEY BLOCK----- From dfbba04bd70c9398ea3e115d446ca2575fe7d7f2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 25 May 2013 23:04:45 +0000 Subject: [PATCH 040/145] Add note to TODO file about python ORMs for public key storage. --- docs/TODO | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/TODO b/docs/TODO index 58e6520..aafbe72 100644 --- a/docs/TODO +++ b/docs/TODO @@ -13,6 +13,13 @@ generated. It would be nice to make the file descriptors for communication with the GnuPG process configurable, and not the default, hard-coded 0=stdin 1=stdout 2=stderr. +** TODO look into RDBMS/ORM for public key storage :io:db: +see http://stackoverflow.com/q/1235594 and http://elixir.ematia.de/trac/wiki + +memcached and pymemcached were the first ones I looked at, then I discovered +redis, which seemed better. At some point we should look into using elixer, +mentioned in the above SO post, so that the backend DB can be chosen freely +and we´re not restricting users to only memcached/cassandra/redis/sqlite/etc. * Key editing :editkey: ** TODO add '--edit-key' feature :editkey: From c683ef31c274c00484782f045cc37cf0aa3fba4a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 26 May 2013 14:04:33 +0000 Subject: [PATCH 041/145] Move old documentation to Sphinx rest file at the end. * It seems annoying to have a bunch of licensing and history lessons as the first thing you read when you look at the documentation. Rather, the first thing you see should be how to install it and use it. --- docs/gnupg.rst | 37 +++++++++++++++++++++++++++++++++++++ src/gnupg.py | 48 +++++------------------------------------------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/docs/gnupg.rst b/docs/gnupg.rst index 0b159f9..323db8c 100644 --- a/docs/gnupg.rst +++ b/docs/gnupg.rst @@ -5,3 +5,40 @@ gnupg Module :members: :undoc-members: :show-inheritance: + +Previous Authors' Documentation +------------------------------- + +Steve Traugott's documentation: + + Portions of this module are derived from A.M. Kuchling's well-designed + GPG.py, using Richard Jones' updated version 1.3, which can be found in + the pycrypto CVS repository on Sourceforge: + + http://pycrypto.cvs.sourceforge.net/viewvc/pycrypto/gpg/GPG.py + + This module is *not* forward-compatible with amk's; some of the old + interface has changed. For instance, since I've added decrypt + functionality, I elected to initialize with a 'gpghome' argument instead + of 'keyring', so that gpg can find both the public and secret keyrings. + I've also altered some of the returned objects in order for the caller to + not have to know as much about the internals of the result classes. + + While the rest of ISconf is released under the GPL, I am releasing this + single file under the same terms that A.M. Kuchling used for pycrypto. + + Steve Traugott, stevegt@terraluna.org + Thu Jun 23 21:27:20 PDT 2005 + + +Vinay Sajip's documentation: + + This version of the module has been modified from Steve Traugott's version + (see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by + Vinay Sajip to make use of the subprocess module (Steve's version uses + os.fork() and so does not work on Windows). Renamed to gnupg.py to avoid + confusion with the previous versions. + + A unittest harness (test_gnupg.py) has also been added. + + Modifications Copyright (C) 2008-2012 Vinay Sajip. All rights reserved. diff --git a/src/gnupg.py b/src/gnupg.py index 95b9c5c..9fb6c3b 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -26,52 +26,14 @@ A.M. Kuchling. This version is patched to sanitize untrusted inputs, due to the necessity of executing :class:`subprocess.Popen([...], shell=True)` in order to communicate -with GnuPG. +with GnuPG. Several speed improvements were also made based on code profiling, +and the API has been cleaned up to support an easier, more Pythonic, +interaction. -:Info: see -:Authors: A.M. Kuchling, Steve Traugott, Vinay Sajip, Isis Lovecruft -:Date: $Date: 2013-04-04 01:11:01 +0000 (Thursday, April 4, 2013) $ -:Description: Documentation of python-gnupg, a Python module for GnuPG. - - -Previous Authors' Documentation -------------------------------- - -Steve Traugott's documentation: - - Portions of this module are derived from A.M. Kuchling's well-designed - GPG.py, using Richard Jones' updated version 1.3, which can be found in - the pycrypto CVS repository on Sourceforge: - - http://pycrypto.cvs.sourceforge.net/viewvc/pycrypto/gpg/GPG.py - - This module is *not* forward-compatible with amk's; some of the old - interface has changed. For instance, since I've added decrypt - functionality, I elected to initialize with a 'gpghome' argument instead - of 'keyring', so that gpg can find both the public and secret keyrings. - I've also altered some of the returned objects in order for the caller to - not have to know as much about the internals of the result classes. - - While the rest of ISconf is released under the GPL, I am releasing this - single file under the same terms that A.M. Kuchling used for pycrypto. - - Steve Traugott, stevegt@terraluna.org - Thu Jun 23 21:27:20 PDT 2005 - - -Vinay Sajip's documentation: - - This version of the module has been modified from Steve Traugott's version - (see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by - Vinay Sajip to make use of the subprocess module (Steve's version uses - os.fork() and so does not work on Windows). Renamed to gnupg.py to avoid - confusion with the previous versions. - - A unittest harness (test_gnupg.py) has also been added. - - Modifications Copyright (C) 2008-2012 Vinay Sajip. All rights reserved. """ + + try: from io import StringIO from io import BytesIO From ae1042d56f99070cd8b2a7648559551d541495ad Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 26 May 2013 17:34:22 +0000 Subject: [PATCH 042/145] Add notes to requirements.txt on dependencies, and remove setuptools-git. --- requirements.txt | 59 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9cef333..15d3051 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,58 @@ -Sphinx>=1.1 +# +# python-gnupg/requirements.txt +# ----------------------------- +# Pip requirements.txt file. This file is also parsed for distribute to use in +# setup.py. +#_____________________________________________________________________________ +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +#______________________________________________________________________________ +# +# Force pip upgrade due to security vulnerabilities. +# +# This has actually has little to do with installing python-gnupg, since +# older versions of pip would install everything just fine. except that, in +# my opinion, using GnuPG for privacy is silly when the installation of +# python-gnupg with an older version of pip is trivially exploitable through +# a MITM attack. see https://github.com/pypa/pip/pull/791 +# +# Also, note that SSL package delivery is *not* entirely fixed yet. See +# https://github.com/TheTorProject/ooni-backend/pull/1#discussion_r4084881 +# +pip>=1.3.1 +# +# NOTE: setuptools is currently (as of 27 May 2013) being merged back into its +# parent project, distribute. By using the included distribute_setup.py +# script, we make sure that we have a recent version of setuptools/distribute, +# which is the *only* Python packaging framework compatible at this point with +# both Python>=2.4 and Python3.x. +# +# A new version of distribute is necessary due to the merging of setuptools +# back into its parent project, distribute. Also, the only way to package for +# both Python 2 and 3 is to use distribute. +# +distribute>=0.6.45 +# +# Sphinx is only necessary for building documentation, so it is added in +# setup.py under extras_require['docs']. +# +# If you want to build the documentation, uncomment this line: +#Sphinx>=1.1 +# +# And, this one is actually used in the gnupg module code: +# psutil>=0.5.1 -setuptools-git>1.0b1 From 494592a6ac109886fdf8982923df5f59b4652fdc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 12:44:52 +0000 Subject: [PATCH 043/145] Add headers to setup.py. --- setup.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/setup.py b/setup.py index 198a1cf..f865ef6 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,29 @@ #!/usr/bin/env python #-*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +#______________________________________________________________________________ +# +# NOTE: setuptools is currently (as of 27 May 2013) being merged back into its +# parent project, distribute. By using the included distribute_setup.py +# script, we make sure that we have a recent version of setuptools/distribute, +# which is the *only* Python packaging framework compatible at this point with +# both Python>=2.4 and Python3.x. +# from distutils.core import setup From 0dc545e51f2bfb0d3ff8c480c7945af2d0a270b6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 28 May 2013 14:14:52 +0000 Subject: [PATCH 044/145] Switch to using distribute instead of setuptools due to projects merging. --- setup.py | 117 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/setup.py b/setup.py index f865ef6..b173396 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,18 @@ # both Python>=2.4 and Python3.x. # -from distutils.core import setup +from __future__ import absolute_import +from __future__ import print_function +import inspect +import os + +## Upgrade setuptools to a version which supports Python 2 and 3 +#os.system('python ./distribute_setup.py') +## Upgrade pip to a version with proper SSL support +#os.system('python ./get-pip.py') + +import setuptools import versioneer versioneer.versionfile_source = 'src/_version.py' versioneer.versionfile_build = 'gnupg/_version.py' @@ -35,39 +45,78 @@ versioneer.parentdir_prefix = 'python-gnupg-' __author__ = "Isis Agora Lovecruft" __contact__ = 'isis@leap.se' +__url__ = 'https://github.com/isislovecruft/python-gnupg \ +https://python-gnupg.readthedocs.org' -setup(name = "python-gnupg", - description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", - long_description = "This module allows easy access to GnuPG's key \ +def get_current_dir(): + """Current dir of this file, regardless of where we're called from.""" + here = inspect.getabsfile(inspect.currentframe()).rsplit(os.path.sep, 1)[0] + return here + +def get_deps_reqs(): + """Get dependencies from the pip requirements.txt file.""" + requirements_file = os.path.join(get_current_dir(), 'requirements.txt') + dependency_links = [] + install_requires = [] + with open(requirements_file) as pipfile: + for line in pipfile.readlines(): + line = line.strip() + if not line.startswith('#'): + if line.startswith('https'): + dependency_links.append(line) + continue + else: + install_requires.append(line) + return dependency_links, install_requires +deps, reqs = get_deps_reqs() +print('%s' % deps) +print('%s' % reqs) + + +def run_distribute_setup_script(): + """Run the setuptools/distribute setup script.""" + script = os.path.join(get_current_dir(), 'distribute_setup.py') + os.system(script) + +setuptools.setup( + name = "python-gnupg", + description="A Python wrapper for GnuPG", + long_description = "This module allows easy access to GnuPG's key \ management, encryption and signature functionality from Python programs. \ It is intended for use with Python 2.6 or greater.", - license="""Copyright © 2013 Isis Lovecruft, et.al. see LICENSE file.""", - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - author=__author__, - author_email=__contact__, - maintainer=__author__, - maintainer_email=__contact__, - url="https://github.com/isislovecruft/python-gnupg", - package_dir={'gnupg': 'src'}, - packages=['gnupg'], - include_package_data=True, - platforms="Linux, BSD, OSX, Windows", - download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", - classifiers=[ - 'Development Status :: 4 - Alpha', - "Intended Audience :: Developers", - 'Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.0", - "Programming Language :: Python :: 3.1", - "Programming Language :: Python :: 3.2", - "Topic :: Software Development :: Libraries :: Python Modules", - 'Classifier:: Topic :: Security :: Cryptography', - 'Classifier:: Topic :: Software Development :: Libraries :: Python Modules', - 'Classifier:: Topic :: Utilities',] - ) + license="""Copyright © 2013 Isis Lovecruft, et.al. see LICENSE file.""", + + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + + author=__author__, + author_email=__contact__, + maintainer=__author__, + maintainer_email=__contact__, + url=__url__, + + packages=setuptools.find_packages(), + + install_requires=reqs, + dependency_links=deps, + extras_require={'docs': ["Sphinx>=1.1"]}, + + platforms="Linux, BSD, OSX, Windows", + download_url="https://github.com/isislovecruft/python-gnupg/archive/master.zip", + classifiers=[ + 'Development Status :: 4 - Alpha', + "Intended Audience :: Developers", + 'Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.0", + "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", + "Topic :: Software Development :: Libraries :: Python Modules", + 'Classifier:: Topic :: Security :: Cryptography', + 'Classifier:: Topic :: Software Development :: Libraries :: Python Modules', + 'Classifier:: Topic :: Utilities',] +) From 8d1e8c71ce0b7c1e52a9d341f5a5c54bc5fd4120 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 29 May 2013 13:14:13 +0000 Subject: [PATCH 045/145] Only import the things we need in test_gnupg.py. --- tests/test_gnupg.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 96d57d1..163ea33 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -23,13 +23,14 @@ A test harness and unittests for gnupg.py. from __future__ import absolute_import from __future__ import print_function +from argparse import ArgumentParser +from codecs import open as open +from functools import wraps +from inspect import getabsfile +from inspect import currentframe from time import gmtime from time import mktime -from functools import wraps - -import argparse -import codecs import encodings import doctest import io @@ -1000,7 +1001,7 @@ if __name__ == "__main__": suite_names.append(name) setattr(GPGTestCase, name, list(methodset)) - parser = argparse.ArgumentParser(description="Unittests for python-gnupg") + parser = ArgumentParser(description="Unittests for python-gnupg") parser.add_argument('--doctest', dest='run_doctest', type=bool, default=False, help='Run example code in docstrings') From e05ab6fd790fa76fe6db90ee25c93110f8f89ed7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 30 May 2013 13:14:13 +0000 Subject: [PATCH 046/145] Add header to package __init__.py. --- src/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/__init__.py b/src/__init__.py index 43a5b8e..2e5695e 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. import gnupg import copyleft From 30860cf4c63b33d4999265b5771f21cd0798c8ae Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 31 May 2013 13:14:13 +0000 Subject: [PATCH 047/145] Switch to py3k absolute imports in package __init__.py. --- src/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 2e5695e..9799906 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -17,19 +17,23 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -import gnupg -import copyleft - -from gnupg import GPG +from __future__ import absolute_import +from . import gnupg +from . import copyleft +from . import _ansistrm +from . import _logger +from . import _meta +from . import _parsers +from . import _util +from .gnupg import GPG from ._version import get_versions -__version__ = get_versions()['version'] -gnupg.__version__ = __version__ gnupg.__author__ = 'Isis Agora Lovecruft' gnupg.__contact__ = 'isis@leap.se' gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg' gnupg.__license__ = copyleft.disclaimer +__version__ = get_versions()['version'] __all__ = ["GPG"] From c0db5604b233ac84a31032f584d7a6c2736532e5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 1 Jun 2013 13:14:13 +0000 Subject: [PATCH 048/145] Add automatic copyright and license retrieval as module attributes. --- src/__init__.py | 7 +- src/copyleft.py | 684 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 684 insertions(+), 7 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 9799906..80fd8a1 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -29,11 +29,10 @@ from . import _util from .gnupg import GPG from ._version import get_versions -gnupg.__author__ = 'Isis Agora Lovecruft' -gnupg.__contact__ = 'isis@leap.se' -gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg' -gnupg.__license__ = copyleft.disclaimer __version__ = get_versions()['version'] +__authors__ = copyleft.authors +__license__ = copyleft.full_text +__copyleft__ = copyleft.copyright __all__ = ["GPG"] diff --git a/src/copyleft.py b/src/copyleft.py index d557030..e1783d8 100644 --- a/src/copyleft.py +++ b/src/copyleft.py @@ -3,9 +3,30 @@ Copyright information for python-gnupg. ''' +from __future__ import absolute_import + +from . import _util + + +authors = { 'lovecruft_isis': _util.author_info( + 'Isis Agora Lovecruft', 'isis@leap.se', '0xA3ADB67A2CDB8B35'), + + 'sajip_vinay': _util.author_info( + 'Vinay Sajip', 'vinay.sajip@gmail.com', '0xDE6EF0B2'), + + 'traugott_steve': _util.author_info( + 'Steve Traugott', 'stevegt@terraluna.org'), + + 'kuchling_am': _util.author_info( + 'A.M. Kuchling', 'amk@amk.ca'), } + copyright = """\ -Copyright (C) 2013 Isis Lovecruft. -See LICENSE for details.""" +Copyright © 2013 Isis Lovecruft + © 2008-2012 Vinay Sajip + © 2005 Steve Traugott + © 2004 A.M. Kuchling +All rights reserved. +See included LICENSE or ``print(gnupg.__license__)`` for full license.""" disclaimer = """\ This file is part of python-gnupg, a Python wrapper around GnuPG. @@ -27,7 +48,7 @@ Where stated, parts of this program were taken from Twisted, which is licensed as follows: Twisted, the Framework of Your Internet -Copyright (c) 2001-2013 Twisted Matrix Laboratories. +Copyright © 2001-2013 Twisted Matrix Laboratories. See LICENSE for details. Permission is hereby granted, free of charge, to any person obtaining @@ -48,3 +69,660 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" + + +AGPLv3_text = """\ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright © 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + + + BEGIN ORIGINAL LICENSE TEXT + +Copyright (c) 2008-2012 by Vinay Sajip. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name(s) of the copyright holder(s) may not be used to endorse or + promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + END ORIGINAL LICENSE TEXT +""" + +full_text = "%s\n\n%s\n\n%s" % (disclaimer, txcopyright, AGPLv3_text) From db7c2760aecf6fced1862f5c242e4cee5299578f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 2 Jun 2013 13:14:13 +0000 Subject: [PATCH 049/145] Add license headers to copyleft.py. INFINITE LICENSE RECURSION! DUN DUN DUN! --- src/copyleft.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/copyleft.py b/src/copyleft.py index e1783d8..b703c7a 100644 --- a/src/copyleft.py +++ b/src/copyleft.py @@ -1,5 +1,22 @@ -#-*- encoding: utf-8 -*- -''' +# -*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +'''copyleft.py +-------------- Copyright information for python-gnupg. ''' From 3c07dfc4fd1142a5d49657f52f8f0b57cf815ebe Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 08:55:45 +0000 Subject: [PATCH 050/145] Fix directly accessible package modules. --- src/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 80fd8a1..3f08b13 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -34,9 +34,13 @@ __authors__ = copyleft.authors __license__ = copyleft.full_text __copyleft__ = copyleft.copyright -__all__ = ["GPG"] +## do not set __package__ = "gnupg", else we will end up with +## gnupg.<*allofthethings*> +__all__ = ["GPG", "_util", "_parsers", "_meta", "_logger"] +## avoid the "from gnupg import gnupg" idiom del gnupg +del absolute_import del copyleft del get_versions del _version From bc5f23fcbeefb3062166d2eb485156db8663b1c6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 08:57:02 +0000 Subject: [PATCH 051/145] Switch to py3k absolute imports in _logger module. --- src/_logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_logger.py b/src/_logger.py index 536b485..fc241bb 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -20,6 +20,7 @@ Logging module for python-gnupg. ''' +from __future__ import absolute_import from __future__ import print_function from datetime import datetime from functools import wraps @@ -28,8 +29,6 @@ import logging import sys import os -import _ansistrm - try: from logging import NullHandler except: @@ -37,6 +36,7 @@ except: def handle(self, record): pass +from . import _ansistrm GNUPG_STATUS_LEVEL = 9 From a6ef75f1f0ebf7927adde3666ecdd1223088f309 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 08:57:49 +0000 Subject: [PATCH 052/145] Switch to py3k absolute imports in _meta module. --- src/_meta.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/_meta.py b/src/_meta.py index 8bfb682..0399e0b 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -17,6 +17,7 @@ # GNU Affero General Public License for more details. +from __future__ import absolute_import from psutil import process_iter from subprocess import Popen from subprocess import PIPE @@ -32,13 +33,13 @@ import locale import os import sys -import _parsers -import _util +from . import _parsers +from . import _util -from _parsers import _check_preferences -from _parsers import _sanitise_list -from _util import log -from _util import _conf +from ._parsers import _check_preferences +from ._parsers import _sanitise_list +from ._util import log +from ._util import _conf class GPGMeta(type): From f042536fe53669fadf790c12d8470badd7282fe4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:01:44 +0000 Subject: [PATCH 053/145] Update GPGBase docstring. --- src/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_meta.py b/src/_meta.py index 0399e0b..88a2041 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -83,7 +83,7 @@ class GPGMeta(type): class GPGBase(object): - """Base class for property storage and controlling process initialisation.""" + """Base class for property storage and to control process initialisation.""" __metaclass__ = GPGMeta From 63181d92ffd0d06a2a1a0ea086a3fcaf7817142e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:49:29 +0000 Subject: [PATCH 054/145] Add keyserver property to GPGBase class. --- src/_meta.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/_meta.py b/src/_meta.py index 88a2041..fd47cbd 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -122,6 +122,8 @@ class GPGBase(object): self._filesystemencoding = encodings.normalize_encoding( sys.getfilesystemencoding().lower()) + self._keyserver = 'hkp://subkeys.pgp.net' + try: assert self.binary, "Could not find binary %s" % binary assert isinstance(verbose, (bool, str, int)), \ @@ -264,6 +266,31 @@ class GPGBase(object): """ self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' + @property + def keyserver(self): + """Get the current keyserver setting.""" + return self._keyserver + + @keyserver.setter + def keyserver(self, location): + """Set the default keyserver to use for sending and receiving keys. + + The ``location`` is sent to :func:`_parsers._check_keyserver` when + option are parsed in :meth:`gnupg.GPG._make_options`. + + :param str location: A string containing the default keyserver. This + should contain the desired keyserver protocol + which is supported by the keyserver, for example, + ``'hkps://keys.mayfirst.org'``. The default + keyserver is ``'hkp://subkeys.pgp.net'``. + """ + self._keyserver = location + + @keyserver.deleter + def keyserver(self): + """Reset the keyserver to the default setting.""" + self._keyserver = 'hkp://subkeys.pgp.net' + def _homedir_getter(self): """Get the directory currently being used as GnuPG's homedir. From 79e3f6f6f5112eaf026a6be10030fba6718714b4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:50:18 +0000 Subject: [PATCH 055/145] Fix symlink creation and removal for gpg binaries in disabled $PATH entries. --- src/_meta.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/_meta.py b/src/_meta.py index fd47cbd..b7d0b97 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -175,8 +175,10 @@ class GPGBase(object): ## symlink our gpg binary into $PWD if the path we are removing is ## the one which contains our gpg executable: + new_gpg_location = os.path.join(os.getcwd(), 'gpg') if gnupg_base == program_base: - os.symlink(self.binary, os.path.join(os.getcwd(), 'gpg')) + os.symlink(self.binary, new_gpg_location) + self.binary = new_gpg_location ## copy the original environment so that we can put it back later: env_copy = os.environ ## this one should not be touched @@ -230,12 +232,11 @@ class GPGBase(object): ## register an _exithandler with the python interpreter: atexit.register(update_path, env_copy, path_copy) - @atexit.register - def remove_symlinked_binary(): - loc = os.path.join(os.getcwd(), 'gpg') - if os.path.islink(loc): - os.unlink(loc) - log.debug("Removed binary symlink '%s'" % loc) + def remove_symlinked_binary(symlink): + if os.path.islink(symlink): + os.unlink(symlink) + log.debug("Removed binary symlink '%s'" % symlink) + atexit.register(remove_symlinked_binary, new_gpg_location) @property def default_preference_list(self): From a4ffcebff8bf9a9600319b31022b93f367bd580f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:51:57 +0000 Subject: [PATCH 056/145] Switch to py3k absolute imports in _util module. --- src/_util.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/_util.py b/src/_util.py index bbfe3c9..a576721 100644 --- a/src/_util.py +++ b/src/_util.py @@ -15,32 +15,36 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -''' -util.py +'''util.py ---------- Extra utilities for python-gnupg. ''' -from datetime import datetime -from socket import gethostname +from __future__ import absolute_import +from datetime import datetime +from inspect import currentframe +from inspect import getabsfile +from socket import gethostname +from time import gmtime +from time import mktime import codecs import encodings import os -import time import threading import random import string import sys -import _logger - try: from io import StringIO from io import BytesIO except ImportError: from cStringIO import StringIO +from . import _logger + + try: unicode _py3k = False @@ -321,7 +325,7 @@ def _make_passphrase(length=None, save=False, file=None): if save: ruid, euid, suid = os.getresuid() gid = os.getgid() - now = time.mktime(time.gmtime()) + now = mktime(gmtime()) if not file: filename = str('passphrase-%s-%s' % uid, now) @@ -379,7 +383,7 @@ def _threaded_copy_data(instream, outstream): def _utc_epoch(): """Get the seconds since epoch for UTC.""" - return int(time.mktime(time.gmtime())) + return int(mktime(gmtime())) def _which(executable, flags=os.X_OK): """Borrowed from Twisted's :mod:twisted.python.proutils . From 2740cd67736dbdc91370081939dfca49cfa69638 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:52:48 +0000 Subject: [PATCH 057/145] Update directory finders. --- src/_util.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/_util.py b/src/_util.py index a576721..a7124f8 100644 --- a/src/_util.py +++ b/src/_util.py @@ -59,10 +59,11 @@ except NameError: ## Directory shortcuts: -_here = os.getcwd() -_test = os.path.join(os.path.join(_here, 'tests'), 'tmp') ## ./tests/tmp -_user = os.environ.get('HOME') ## $HOME -_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg +#_here = os.getcwd() +_here = getabsfile(currentframe()).rsplit(os.path.sep, 1)[0] ## current dir +_test = os.path.join(os.path.join(_here, 'test'), 'tmp') ## ./tests/tmp +_user = os.environ.get('HOME') ## $HOME +_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg _conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg') ## $HOME/.config/python-gnupg From f5d150441ca8c2a03389233ca674dc69ac07b430 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:53:27 +0000 Subject: [PATCH 058/145] Add _util.author_info helper for copyleft module. --- src/_util.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/_util.py b/src/_util.py index a7124f8..e225838 100644 --- a/src/_util.py +++ b/src/_util.py @@ -105,6 +105,16 @@ def find_encodings(enc=None, system=False): return coder +def author_info(name, contact=None, public_key=None): + """Easy object-oriented representation of contributor info. + + :param str name: The contributor´s name. + :param str contact: The contributor´s email address or contact + information, if given. + :param str public_key: The contributor´s public keyid, if given. + """ + return Storage(name=name, contact=contact, public_key=public_key) + def _copy_data(instream, outstream): """Copy data from one stream to another. @@ -473,3 +483,45 @@ class InheritableProperty(object): self.fdel(obj) else: getattr(obj, self.fdel.__name__)() + +class Storage(dict): + """A dictionary where keys are stored as class attributes. + + For example, ``obj.foo`` can be used in addition to ``obj['foo']``: + + >>> o = Storage(a=1) + >>> o.a + 1 + >>> o['a'] + 1 + >>> o.a = 2 + >>> o['a'] + 2 + >>> del o.a + >>> o.a + None + """ + def __getattr__(self, key): + try: + return self[key] + except KeyError, k: + return None + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + try: + del self[key] + except KeyError as k: + raise AttributeError(k.message) + + def __repr__(self): + return '' + + def __getstate__(self): + return dict(self) + + def __setstate__(self, value): + for (k, v) in value.items(): + self[k] = v From 5e6a0d215d8a58d3545fc70764914cbef54feb6c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:54:11 +0000 Subject: [PATCH 059/145] Fix a property function signature which asked for an extraneous arg. --- src/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_meta.py b/src/_meta.py index b7d0b97..33be7c1 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -255,7 +255,7 @@ class GPGBase(object): self._prefs = prefs @default_preference_list.deleter - def default_preference_list(self, prefs): + def default_preference_list(self): """Reset the default preference list to its original state. Note that "original state" does not mean the default preference From 67ac4b31f99d3d9e4d0a89f4ca52fafcbe3da0b2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:55:58 +0000 Subject: [PATCH 060/145] Switch to py3k absolute imports in _parsers module. --- src/_parsers.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 6235a03..98ba8de 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -15,17 +15,18 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -''' -parsers.py ----------- +'''parsers.py +------------- Classes for parsing GnuPG status messages and sanitising commandline options. ''' +from __future__ import absolute_import +from __future__ import print_function + import re -from _util import log - -import _util +from . import _util +from ._util import log ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) From 8463cc2cee0e77fc1738ad316f56d8de140e0ed3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 09:56:56 +0000 Subject: [PATCH 061/145] Add _check_keyserver parser for GPG.keyserver property. --- src/_parsers.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/_parsers.py b/src/_parsers.py index 98ba8de..4bc54e5 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -40,6 +40,34 @@ class UsageError(Exception): """Raised when incorrect usage of the API occurs..""" +def _check_keyserver(location): + """Check that a given keyserver is a known protocol and does not contain + shell escape characters. + + :param str location: A string containing the default keyserver. This + should contain the desired keyserver protocol which + is supported by the keyserver, for example, the + default is ``'hkp://subkeys.pgp.net'``. + :rtype: str or None + :returns: A string specifying the protocol and keyserver hostname, if the + checks passed. If not, returns None. + """ + protocols = ['hkp://', 'hkps://', 'http://', 'https://', 'ldap://', + 'mailto:'] ## xxx feels like i´m forgetting one... + for proto in protocols: + if location.startswith(proto): + url = location.replace(proto, str()) + host, slash, extra = url.partition('/') + if extra: log.warn("URI text for %s: '%s'" % (host, extra)) + log.debug("Got host string for keyserver setting: '%s'" % host) + + host = _fix_unsafe(host) + if host: + log.debug("Cleaned host string: '%s'" % host) + keyserver = proto + host + return keyserver + return None + def _check_preferences(prefs, pref_type=None): """Check cipher, digest, and compression preference settings. From 68157a0777219d675cd5130f78f8f2e76ce81c36 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:07:54 +0000 Subject: [PATCH 062/145] Shorten and exception message string. --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 4bc54e5..5dddd07 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -91,7 +91,7 @@ def _check_preferences(prefs, pref_type=None): elif isinstance(prefs, list): prefs = set(prefs) else: - msg = "prefs must be a list of strings, or one space-separated string" + msg = "prefs must be list of strings, or space-separated string" log.error("parsers._check_preferences(): %s" % message) raise TypeError(message) From a03d666d0a02a78d1dfd7a50d49a3bfb3c8569a1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:09:16 +0000 Subject: [PATCH 063/145] Implement --recieve-keys. --- src/_meta.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/_meta.py b/src/_meta.py index 33be7c1..155ff62 100644 --- a/src/_meta.py +++ b/src/_meta.py @@ -517,3 +517,24 @@ class GPGBase(object): writer = _util._threaded_copy_data(file, stdin) self._collect_output(p, result, writer, stdin) return result + + def _recv_keys(self, keyids, keyserver=None): + """Import keys from a keyserver. + + :param str keyids: A space-delimited string containing the keyids to + request. + :param str keyserver: The keyserver to request the ``keyids`` from; + defaults to :property:`gnupg.GPG.keyserver`. + """ + if not keyserver: + keyserver = self.keyserver + + args = ['--keyserver {}'.format(keyserver), + '--recv-keys {}'.format(keyids)] + log.info('Requesting keys from %s: %s' % (keyserver, keyids)) + + result = self._result_map['import'](self) + proc = self._open_subprocess(args) + self._collect_output(proc, result) + log.debug('recv_keys result: %r', result.__dict__) + return result From 2c95074006291b6108dcf1d4945f5e202e8a8023 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:15:42 +0000 Subject: [PATCH 064/145] Add checks for keyserver and recv-keys options to _parsers module. --- src/_parsers.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 5dddd07..9b00a68 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -389,6 +389,9 @@ def _is_allowed(input): '--no-default-keyring', '--default-key', '--no-options', + '--keyserver', + '--recv-keys', + '--send-keys', ## preferences '--digest-algo', '--cipher-algo', @@ -530,11 +533,19 @@ def _sanitise(*args): ## because they are only allowed if the pass the regex if flag in ['--default-key', '--recipient', '--export', '--export-secret-keys', '--delete-keys', - '--list-sigs', '--export-secret-subkeys',]: - if _is_hex(v): - safe_option += (v + " ") - continue + '--list-sigs', '--export-secret-subkeys', + '--recv-keys']: + if _is_hex(v): safe_option += (v + " ") else: log.debug("'%s %s' not hex." % (flag, v)) + continue + + elif flag in ['--keyserver']: + host = _check_keyserver(v) + if host: + log.debug("Setting keyserver: %s" % host) + safe_option += (v + " ") + else: log.debug("Dropping keyserver: %s" % v) + continue val = _fix_unsafe(v) From 776584bd598113083155d466f586b8601cbcaf67 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:16:38 +0000 Subject: [PATCH 065/145] Clean up an assert in _parsers. --- src/_parsers.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 9b00a68..8e578cc 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -413,14 +413,12 @@ def _is_allowed(input): '--fingerprint', ]) - ## check that allowed is a subset of possible try: - assert allowed.issubset(possible), \ - 'allowed is not subset of known options, difference: %s' \ - % allowed.difference(possible) - except AssertionError as ae: - log.error("_is_allowed(): %s" % ae.message) - raise UsageError(ae.message) + ## check that allowed is a subset of possible + assert allowed.issubset(possible) + except AssertionError: + raise UsageError("'allowed' isn't a subset of known options, diff: %s" + % allowed.difference(possible)) ## if we got a list of args, join them ## From e00597c3edcc6314a475c173d35176e3d9caa2e1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:17:07 +0000 Subject: [PATCH 066/145] Check .isspace() instead of a literal string for speed. --- src/_parsers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 8e578cc..5b3d4e7 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -515,14 +515,14 @@ def _sanitise(*args): except (AssertionError, ProtectedOption) as error: log.warn("_check_option(): %s" % error.message) else: - safe_option += (flag + " ") + safe_option += (flag + ' ') if (not _util._py3k and isinstance(value, basestring)) \ or (_util._py3k and isinstance(value, str)): values = value.split(' ') for v in values: try: assert v is not None - assert v.strip() != "" + assert not v.isspace() except: log.debug("Dropping %s %s" % (flag, v)) continue @@ -549,7 +549,7 @@ def _sanitise(*args): try: assert v is not None - assert v.strip() != "" + assert not v.isspace() except: log.debug("Dropping %s %s" % (flag, v)) continue From 2f16ca18f09db544029f9c0c5ed53437f3a7f57f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:19:36 +0000 Subject: [PATCH 067/145] Clean and fix several things in the ImportResult parser. * Fix the strange check for "int(code) | int(reason) == int(code)". * Hide a bunch of the attributes which are usually "0" and put them into an OrderedDict attribute. This needs to be an OrderedDict because the IMPORT_RES status code needs to line up with the contents correctly. --- src/_parsers.py | 76 +++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 5b3d4e7..4c9da3a 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -23,6 +23,7 @@ Classes for parsing GnuPG status messages and sanitising commandline options. from __future__ import absolute_import from __future__ import print_function +import collections import re from . import _util @@ -888,13 +889,25 @@ class ImportResult(object): :type gpg: :class:`gnupg.GPG` :param gpg: An instance of :class:`gnupg.GPG`. """ + _ok_reason = {'0': 'Not actually changed', + '1': 'Entirely new key', + '2': 'New user IDs', + '4': 'New signatures', + '8': 'New subkeys', + '16': 'Contains private key', + '17': 'Contains private key',} - counts = '''count no_user_id imported imported_rsa unchanged - n_uids n_subk n_sigs n_revoc sec_read sec_imported - sec_dups not_imported'''.split() + _problem_reason = { '0': 'No specific reason given', + '1': 'Invalid Certificate', + '2': 'Issuer Certificate missing', + '3': 'Certificate Chain too long', + '4': 'Error storing certificate', } - #: List of all keys imported. - imported = list() + _fields = '''count no_user_id imported imported_rsa unchanged + n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups + not_imported'''.split() + _counts = collections.OrderedDict( + zip(_fields, [int(0) for x in xrange(len(_fields))]) ) #: A list of strings containing the fingerprints of the GnuPG keyIDs #: imported. @@ -906,8 +919,7 @@ class ImportResult(object): def __init__(self, gpg): self._gpg = gpg - for result in self.counts: - setattr(self, result, None) + self.counts = self._counts def __nonzero__(self): """Override the determination for truthfulness evaluation. @@ -915,25 +927,11 @@ class ImportResult(object): :rtype: bool :returns: True if we have immport some keys, False otherwise. """ - if self.not_imported: return False - if not self.fingerprints: return False + if self.counts.not_imported > 0: return False + if len(self.fingerprints) == 0: return False return True __bool__ = __nonzero__ - ok_reason = {'0': 'Not actually changed', - '1': 'Entirely new key', - '2': 'New user IDs', - '4': 'New signatures', - '8': 'New subkeys', - '16': 'Contains private key', - '17': 'Contains private key',} - - problem_reason = { '0': 'No specific reason given', - '1': 'Invalid Certificate', - '2': 'Issuer Certificate missing', - '3': 'Certificate Chain too long', - '4': 'Error storing certificate', } - def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. @@ -944,16 +942,16 @@ class ImportResult(object): pass elif key == "NODATA": self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'No valid data found'}) + 'status': 'No valid data found'}) elif key == "IMPORT_OK": reason, fingerprint = value.split() reasons = [] - for code, text in self.ok_reason.items(): - if int(reason) | int(code) == int(reason): + for code, text in self._ok_reason.items(): + if int(reason) == int(code): reasons.append(text) reasontext = '\n'.join(reasons) + "\n" self.results.append({'fingerprint': fingerprint, - 'ok': reason, 'text': reasontext}) + 'status': reasontext}) self.fingerprints.append(fingerprint) elif key == "IMPORT_PROBLEM": try: @@ -962,25 +960,29 @@ class ImportResult(object): reason = value fingerprint = '' self.results.append({'fingerprint': fingerprint, - 'problem': reason, 'text': self.problem_reason[reason]}) + 'status': self._problem_reason[reason]}) elif key == "IMPORT_RES": import_res = value.split() - for i in range(len(self.counts)): - setattr(self, self.counts[i], int(import_res[i])) + for x in range(len(self.counts)): + self.counts[self.counts.keys()[x]] = int(import_res[x]) elif key == "KEYEXPIRED": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Key expired'}) + res = {'fingerprint': None, + 'status': 'Key expired'} + self.results.append(res) + ## Accoring to docs/DETAILS L859, SIGEXPIRED is obsolete: + ## "Removed on 2011-02-04. This is deprecated in favor of KEYEXPIRED." elif key == "SIGEXPIRED": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Signature expired'}) + res = {'fingerprint': None, + 'status': 'Signature expired'} + self.results.append(res) else: raise ValueError("Unknown status message: %r" % key) def summary(self): l = [] - l.append('%d imported' % self.imported) - if self.not_imported: - l.append('%d not imported' % self.not_imported) + l.append('%d imported' % self.counts['imported']) + if self.counts['not_imported']: + l.append('%d not imported' % self.counts['not_imported']) return ', '.join(l) From cdb9e5f4ea0d4adb2f92151d7f7558ec8954f0e1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:24:12 +0000 Subject: [PATCH 068/145] Add better documentation for Verify class and class attributes. --- src/_parsers.py | 102 ++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 4c9da3a..0cc54d2 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -987,12 +987,35 @@ class ImportResult(object): class Verify(object): - """Parser for internal status messages from GnuPG for - certification/signature verification, and for parsing portions of status - messages from decryption operations. + """Parser for status messages from GnuPG for certifications and signature + verifications. - :type gpg: :class:`gnupg.GPG` - :param gpg: An instance of :class:`gnupg.GPG`. + People often mix these up, or think that they are the same thing. While it + is true that certifications and signatures *are* the same cryptographic + operation -- and also true that both are the same as the decryption + operation -- a distinction is made for important reasons. + + A certification: + * is made on a key, + * can help to validate or invalidate the key owner's identity, + * can assign trust levels to the key (or to uids and/or subkeys that + the key contains), + * and can be used in absense of in-person fingerprint checking to try + to build a path (through keys whose fingerprints have been checked) + to the key, so that the identity of the key's owner can be more + reliable without having to actually physically meet in person. + + A signature: + * is created for a file or other piece of data, + * can help to prove that the data hasn't been altered, + * and can help to prove that the data was sent by the person(s) in + possession of the private key that created the signature, and for + parsing portions of status messages from decryption operations. + + There are probably other things unique to each that have been + scatterbrainedly omitted due to the programmer sitting still and staring + at GnuPG debugging logs for too long without snacks, but that is the gist + of it. """ TRUST_UNDEFINED = 0 @@ -1007,49 +1030,44 @@ class Verify(object): "TRUST_FULLY" : TRUST_FULLY, "TRUST_ULTIMATE" : TRUST_ULTIMATE,} - #: True if the signature is valid, False otherwise. - valid = False - #: A string describing the status of the signature verification. - #: Can be one of ``'signature bad'``, ``'signature good'``, - #: ``'signature valid'``, ``'signature error'``, ``'decryption failed'``, - #: ``'no public key'``, ``'key exp'``, or ``'key rev'``. - status = None - #: The fingerprint of the signing keyid. - fingerprint = None - #: The fingerprint of the corresponding public key, which may be different - #: if the signature was created with a subkey. - pubkey_fingerprint = None - #: The keyid of the signing key. - key_id = None - #: The id of the signature itself. - signature_id = None - #: The creation date of the signing key. - creation_date = None - #: The timestamp of the purported signature, if we are unable to parse it. - timestamp = None - #: The userid of the signing key which was used to create the signature. - username = None - #: When the signing key is due to expire. - expire_timestamp = None - #: The timestamp for when the signature was created. - sig_timestamp = None - #: A number 0-4 describing the trust level of the signature. - trust_level = None - #: The string corresponding to the ``trust_level`` number. - trust_text = None - def __init__(self, gpg): + """Create a parser for verification and certification commands. + + :param gpg: An instance of :class:`gnupg.GPG`. + """ self._gpg = gpg + #: True if the signature is valid, False otherwise. self.valid = False - self.fingerprint = self.creation_date = self.timestamp = None - self.signature_id = self.key_id = None - self.username = None + #: A string describing the status of the signature verification. + #: Can be one of ``signature bad``, ``signature good``, + #: ``signature valid``, ``signature error``, ``decryption failed``, + #: ``no public key``, ``key exp``, or ``key rev``. self.status = None + #: The fingerprint of the signing keyid. + self.fingerprint = None + #: The fingerprint of the corresponding public key, which may be + #: different if the signature was created with a subkey. self.pubkey_fingerprint = None - self.expire_timestamp = None + #: The keyid of the signing key. + self.key_id = None + #: The id of the signature itself. + self.signature_id = None + #: The creation date of the signing key. + self.creation_date = None + #: The timestamp of the purported signature, if we are unable to parse + #: and/or validate it. + self.timestamp = None + #: The timestamp for when the valid signature was created. self.sig_timestamp = None - self.trust_text = None + #: The userid of the signing key which was used to create the + #: signature. + self.username = None + #: When the signing key is due to expire. + self.expire_timestamp = None + #: An integer 0-4 describing the trust level of the signature. self.trust_level = None + #: The string corresponding to the ``trust_level`` number. + self.trust_text = None def __nonzero__(self): """Override the determination for truthfulness evaluation. @@ -1130,7 +1148,7 @@ class Crypt(Verify): self._gpg = gpg self.data = '' self.ok = False - self.status = '' + self.status = None self.data_format = None self.data_timestamp = None self.data_filename = None From f9948c7c7a1e375f4117b9d8ab9466fe662a7937 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:25:09 +0000 Subject: [PATCH 069/145] Fix ListPackets to set self.status for messages, like the other parsers. --- src/_parsers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 0cc54d2..3644094 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -1210,14 +1210,14 @@ class Crypt(Verify): super(Crypt, self)._handle_status(key, value) class ListPackets(object): - """ - Handle status messages for --list-packets. - """ + """Handle status messages for --list-packets.""" def __init__(self, gpg): self._gpg = gpg - self.nodata = None - self.key = None + #: A string describing the current processing status, or error, if one + #: has occurred. + self.status = None + self.key_id = None self.need_passphrase = None self.need_passphrase_sym = None self.userid_hint = None @@ -1228,7 +1228,7 @@ class ListPackets(object): :raises: :exc:`ValueError` if the status message is unknown. """ if key == 'NODATA': - self.nodata = True + self.status = nodata(value) elif key == 'ENC_TO': # This will only capture keys in our keyring. In the future we # may want to include multiple unknown keys in this list. From 5c49b5826cf56da89eb1474ec248e46579e8a97d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:26:32 +0000 Subject: [PATCH 070/145] Redefine cleanup operations in the Makefile. --- Makefile | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index eeaf3a0..932f149 100644 --- a/Makefile +++ b/Makefile @@ -7,26 +7,18 @@ ctags: etags: find . -name "*.py" -print | xargs etags -cleanup-src: - cd src && \ - rm -f \#*\# && \ - rm -f ./*.pyc && \ - rm -f ./*.pyo +# Sanitation targets -- clean leaves libraries, executables and tags +# files, which clobber removes as well +pycremoval: + find . -name '*.py[co]' -exec rm -f {} ';' -cleanup-tests: - cd tests && \ - rm -f \#*\# && \ - rm -f ./*.pyc && \ - rm -f ./*.pyo - mkdir -p tests/tmp - mkdir -p tests/logs - touch tests/placeholder.log - mv tests/*.log tests/logs/ - rm tests/logs/placeholder.log - touch placeholder.log - rm *.log - touch tests/random_seed_is_sekritly_pi - rm tests/random_seed* +cleanup-src: pycremoval + cd gnupg && rm -f \#*\# + +cleanup-tests: cleanup-src + cd $(TESTDIR) && rm -f \#*\# + mkdir -p gnupg/test/tmp + mkdir -p gnupg/test/logs cleanup-tests-all: cleanup-tests rm -rf tests/tmp From 0f9a7f5faf01de6b2a748c31f47ebf18788384f1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:28:35 +0000 Subject: [PATCH 071/145] Redefine commands for running unittests in the Makefile. --- Makefile | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 932f149..ac299af 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +SHELL=/bin/sh +TESTDIR=./gnupg/test +TESTHANDLE=$(TESTDIR)/test_gnupg.py +FILES=$(SHELL find ./gnupg/ -name "*.py" -printf "%p,") + .PHONY=all all: uninstall install test @@ -27,19 +32,22 @@ cleanup-build: mkdir buildnot rm -rf build* -test: cleanup-src cleanup-tests - which gpg - gpg --version - which gpg2 - gpg2 --version +test-before: cleanup-src cleanup-tests + which gpg && gpg --version + which gpg2 && gpg2 --version which gpg-agent which pinentry - which python - python --version - which pip - pip --version - pip list - python tests/test_gnupg.py parsers basic encodings genkey sign listkeys crypt keyrings import + which python && python --version + which pip && pip --version && pip list + +test: test-before + python $(TESTHANDLE) basic encodings parsers keyrings listkeys genkey \ + sign crypt + touch gnupg/test/placeholder.log + mv gnupg/test/*.log gnupg/test/logs/ + rm gnupg/test/logs/placeholder.log + touch gnupg/test/random_seed_is_sekritly_pi + rm gnupg/test/random_seed* install: python setup.py install --record installed-files.txt From bddabb140631ac373fc0f2239a1e2df82c00b9b8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:29:32 +0000 Subject: [PATCH 072/145] Add a reinstall command. --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index ac299af..d51fa16 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,8 @@ uninstall: touch installed-files.txt cat installed-files.txt | sudo xargs rm -rf +reinstall: uninstall install + cleandocs: sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs src/ tests/ From 2b54a03922c85e6216cbb8cdb4a7f402db89d2c4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:30:09 +0000 Subject: [PATCH 073/145] Redefine document build commands in Makefile. --- Makefile | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index d51fa16..e8b42e1 100644 --- a/Makefile +++ b/Makefile @@ -59,12 +59,10 @@ uninstall: reinstall: uninstall install cleandocs: - sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs src/ tests/ + sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" \ + -o docs gnupg/ tests/ docs: - cd docs - make clean - make html - -venv: - -source /usr/shared/python/ns/virtualenvwrapper.sh && mkvirtualenv -a "$PWD" --no-site-packages --unzip-setuptools --distribute python-gnupg + cd docs && \ + make clean && \ + make html From dbb8e395cc91057225d1a6ddf336f2e514618d69 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:31:21 +0000 Subject: [PATCH 074/145] Use the copyleft.authors objects in module documentation for gnupg. --- src/gnupg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index 9fb6c3b..88ea97e 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -30,6 +30,9 @@ with GnuPG. Several speed improvements were also made based on code profiling, and the API has been cleaned up to support an easier, more Pythonic, interaction. +:authors: see ``gnupg.__authors__`` +:license: see ``gnupg.__license__`` +:info: see """ From 36a6bf712ac153056abd19f92ec694099ae6f28c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:32:25 +0000 Subject: [PATCH 075/145] Switch to using py3k absolute imports in gnupg module. --- src/gnupg.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 88ea97e..7d4baa0 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -35,7 +35,12 @@ interaction. :info: see """ +from __future__ import absolute_import +from codecs import open as open +import encodings +import os +import textwrap try: from io import StringIO @@ -43,20 +48,15 @@ try: except ImportError: from cStringIO import StringIO -from codecs import open as open - -import encodings -import os - -import _parsers -import _util - -from _meta import GPGBase -from _parsers import _fix_unsafe -from _util import _is_list_or_tuple -from _util import _is_stream -from _util import _make_binary_stream -from _util import log +## see PEP-328 http://docs.python.org/2.5/whatsnew/pep-328.html +from . import _parsers +from . import _util +from ._meta import GPGBase +from ._parsers import _fix_unsafe +from ._util import _is_list_or_tuple +from ._util import _is_stream +from ._util import _make_binary_stream +from ._util import log class GPG(GPGBase): From eb1967600e023bcf8408b2312211d311c2cdbfe9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:33:05 +0000 Subject: [PATCH 076/145] Remove unnecessary io.BytesIO import from gnupg module. --- src/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 7d4baa0..309b4aa 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -44,7 +44,6 @@ import textwrap try: from io import StringIO - from io import BytesIO except ImportError: from cStringIO import StringIO From ca28c1a4d202ce67168f29f35c3fd73d4af3289f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:33:44 +0000 Subject: [PATCH 077/145] Remove a kwarg from GPG.__init__() since properties are settable in child cls. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 309b4aa..e39d90b 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -67,7 +67,7 @@ class GPG(GPGBase): def __init__(self, binary=None, homedir=None, verbose=False, use_agent=False, keyring=None, secring=None, - default_preference_list=None, options=None): + options=None): """Initialize a GnuPG process wrapper. :param str binary: Name for GnuPG binary executable. If the absolute From d3c509265f2439ad47481c9e48152381ce89ef7e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:34:26 +0000 Subject: [PATCH 078/145] Remove old documentation on no-longer-existant kwarg for GPG.__init__(). --- src/gnupg.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index e39d90b..ae0add2 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -88,11 +88,6 @@ class GPG(GPGBase): 'secring.gpg' in the :param:homedir directory, and create that file if it does not exist. - :param str pubring: Name of alternative public keyring file to use. If - left unspecified, this will default to using - 'pubring.gpg' in the :param:homedir directory, and - create that file if it does not exist. - :param list options: A list of additional options to pass to the GPG binary. From 0f2770ff78bb275481d7c40bcf4ffd5fdf4467be Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:35:26 +0000 Subject: [PATCH 079/145] Update doctest for GPG.__init__(). --- src/gnupg.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index ae0add2..99bda6e 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -98,21 +98,15 @@ class GPG(GPGBase): >>> import gnupg GnuPG logging disabled... - >>> gpg = gnupg.GPG(homedir='./test-homedir') + >>> gpg = gnupg.GPG(homedir='doctests') >>> gpg.keyring - './test-homedir/pubring.gpg' + './doctests/pubring.gpg' >>> gpg.secring - './test-homedir/secring.gpg' + './doctests/secring.gpg' >>> gpg.use_agent False >>> gpg.binary '/usr/bin/gpg' - >>> import os - >>> import shutil - >>> if os.path.exists('./test-homedir'): - ... shutil.rmtree('./test-homedir') - ... - """ super(GPG, self).__init__( From 64f093d434fc255b5da97320c293c8d2d20f0aa3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:35:59 +0000 Subject: [PATCH 080/145] Add GPG.keyserver to settings printout. --- src/gnupg.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 99bda6e..05c2a4d 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -114,24 +114,24 @@ class GPG(GPGBase): home=homedir, keyring=keyring, secring=secring, - default_preference_list=default_preference_list, options=options, verbose=verbose, use_agent=use_agent,) - log.info(""" -Initialised settings: -binary: %s -homedir: %s -keyring: %s -secring: %s -default_preference_list: %s -options: %s -verbose: %s -use_agent: %s + log.info(textwrap.dedent(""" + Initialised settings: + binary: %s + homedir: %s + keyring: %s + secring: %s + default_preference_list: %s + keyserver: %s + options: %s + verbose: %s + use_agent: %s """ % (self.binary, self.homedir, self.keyring, self.secring, - self.default_preference_list, self.options, str(self.verbose), - str(self.use_agent))) + self.default_preference_list, self.keyserver, self.options, + str(self.verbose), str(self.use_agent)))) self._batch_dir = os.path.join(self.homedir, 'batch-files') self._key_dir = os.path.join(self.homedir, 'generated-keys') From b1760735ea86f059400a5eea7347e081715b726e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:37:03 +0000 Subject: [PATCH 081/145] Fix doctests in GPG class to all use the same homedir directory. --- src/gnupg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 05c2a4d..484577c 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -242,7 +242,7 @@ class GPG(GPGBase): def verify(self, data): """Verify the signature on the contents of the string ``data``. - >>> gpg = GPG(homedir="keys") + >>> gpg = GPG(homedir="doctests") >>> input = gpg.gen_key_input(Passphrase='foo') >>> key = gpg.gen_key(input) >>> assert key @@ -447,8 +447,8 @@ class GPG(GPGBase): include it in the args. >>> import shutil - >>> shutil.rmtree("keys") - >>> gpg = GPG(homedir="keys") + >>> shutil.rmtree("doctests") + >>> gpg = GPG(homedir="doctests") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -502,7 +502,7 @@ class GPG(GPGBase): """Get the signatures for each of the ``keyids``. >>> import gnupg - >>> gpg = gnupg.GPG(homedir="./tests/doctests") + >>> gpg = gnupg.GPG(homedir="doctests") >>> key_input = gpg.gen_key_input() >>> key = gpg.gen_key(key_input) >>> assert key.fingerprint @@ -532,7 +532,7 @@ class GPG(GPGBase): :meth:`GPG.gen_key_input()` for creating the control input. >>> import gnupg - >>> gpg = gnupg.GPG(homedir="./tests/doctests") + >>> gpg = gnupg.GPG(homedir="doctests") >>> key_input = gpg.gen_key_input() >>> key = gpg.gen_key(key_input) >>> assert key.fingerprint @@ -603,7 +603,7 @@ class GPG(GPGBase): >>> import gnupg GnuPG logging disabled... >>> from __future__ import print_function - >>> gpg = gnupg.GPG(homedir='./tests/doctests') + >>> gpg = gnupg.GPG(homedir='doctests') >>> alice = { 'name_real': 'Alice', ... 'name_email': 'alice@inter.net', ... 'expire_date': '2014-04-01', From 64028912dec1e9d131b0aa84e892e0c2547a6387 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:38:18 +0000 Subject: [PATCH 082/145] Rewrite GPG.recv_keys to hook into GPGBase.recv_keys. --- src/gnupg.py | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 484577c..5e38295 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -344,33 +344,23 @@ class GPG(GPGBase): data.close() return result - def recv_keys(self, keyserver, *keyids): - """Import a key from a keyserver + def recv_keys(self, *keyids, **kwargs): + """Import keys from a keyserver. + + :param str keyids: Each ``keyids`` argument should be a string + containing a keyid to request. + :param str keyserver: The keyserver to request the ``keyids`` from; + defaults to :property:`gnupg.GPG.keyserver`. - >>> import shutil - >>> shutil.rmtree("doctests") >>> gpg = gnupg.GPG(homedir="doctests") - >>> result = gpg.recv_keys('pgp.mit.edu', '3FF0DB166A7476EA') - >>> assert result - + >>> key = gpg.recv_keys('hkp://pgp.mit.edu', '3FF0DB166A7476EA') + >>> assert key """ - safe_keyserver = _fix_unsafe(keyserver) - - result = self._result_map['import'](self) - data = _make_binary_stream("", self._encoding) - args = ['--keyserver', keyserver, '--recv-keys'] - if keyids: - if keyids is not None: - safe_keyids = ' '.join( - [(lambda: _fix_unsafe(k))() for k in keyids]) - log.debug('recv_keys: %r', safe_keyids) - args.extend(safe_keyids) - - self._handle_io(args, data, result, binary=True) - data.close() - log.debug('recv_keys result: %r', result.__dict__) - return result + keys = ' '.join([key for key in keyids]) + return self._recv_keys(keys, **kwargs) + else: + log.error("No keyids requested for --recv-keys!") def delete_keys(self, fingerprints, secret=False, subkeys=False): """Delete a key, or list of keys, from the current keyring. From 118b7f5a28673974d114b52da0518a107a8b742f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:39:16 +0000 Subject: [PATCH 083/145] Fix a broken directory path in GPG.gen_key() doctest. --- src/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 5e38295..1645d20 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -615,8 +615,8 @@ class GPG(GPGBase): Name-Email: alice@inter.net Key-Length: 4096 Subkey-Length: 4096 - %pubring ./tests/doctests/pubring.gpg - %secring ./tests/doctests/secring.gpg + %pubring ./doctests/alice.pubring.gpg + %secring ./doctests/alice.secring.gpg %commit >>> alice_key = gpg.gen_key(alice_input) From e573136666ad24ad98fd20be4ecec23f2a4274eb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:39:56 +0000 Subject: [PATCH 084/145] Rewrite doctest for GPG.encrypt. --- src/gnupg.py | 51 +++++++++++++++------------------------------------ 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 1645d20..3e63783 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -986,42 +986,21 @@ generate keys. Please see """Encrypt the message contained in ``data`` to ``recipients``. >>> import shutil - >>> if os.path.exists("keys"): - ... shutil.rmtree("keys") - >>> gpg = GPG(homedir="keys") - >>> input = gpg.gen_key_input(passphrase='foo') - >>> result = gpg.gen_key(input) - >>> print1 = result.fingerprint - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> print2 = result.fingerprint - >>> result = gpg.encrypt("hello",print2) - >>> message = str(result) - >>> assert message != 'hello' - >>> result = gpg.decrypt(message) - >>> assert result - >>> str(result) - 'hello' - >>> result = gpg.encrypt("hello again",print1) - >>> message = str(result) - >>> result = gpg.decrypt(message,passphrase='bar') - >>> result.status in ('decryption failed', 'bad passphrase') - True - >>> assert not result - >>> result = gpg.decrypt(message,passphrase='foo') - >>> result.status == 'decryption ok' - True - >>> str(result) - 'hello again' - >>> result = gpg.encrypt("signed hello",print2,sign=print1,passphrase='foo') - >>> result.status == 'encryption ok' - True - >>> message = str(result) - >>> result = gpg.decrypt(message) - >>> result.status == 'decryption ok' - True - >>> assert result.fingerprint == print1 - + >>> if os.path.exists("doctests"): + ... shutil.rmtree("doctests") + >>> gpg = GPG(homedir="doctests") + >>> key_settings = gpg.gen_key_input(key_type='RSA', + ... key_length=1024, + ... key_usage='ESCA', + ... passphrase='foo') + >>> key = gpg.gen_key(key_settings) + >>> encrypted = gpg.encrypt("sekrit", key.printprint) + >>> message = str(encrypted) + >>> assert message != 'sekrit' + >>> decrypted = gpg.decrypt(message) + >>> assert decrypted + >>> str(decrypted) + 'sekrit' """ stream = _make_binary_stream(data, self._encoding) result = self.encrypt_file(stream, recipients, **kwargs) From 4584719c5b63a4ba748c6f04ee0773e2f73c7fb4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:49:53 +0000 Subject: [PATCH 085/145] Deduplicate code in GPGWrapper. --- src/gnupg.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 3e63783..cf2791f 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1065,19 +1065,6 @@ class GPGWrapper(GPG): raise LookupError( "GnuPG public key for subkey %s not found!" % subkey) - def encrypt(self, data, recipient, default_key=None, always_trust=True, - passphrase=None, symmetric=False): - """ - Encrypt data using GPG. - """ - # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).encrypt(data, recipient, - default_key=default_key, - always_trust=always_trust, - passphrase=passphrase, - symmetric=symmetric, - cipher_algo='AES256') - def send_keys(self, keyserver, *keyids): """Send keys to a keyserver.""" result = self._result_map['list'](self) From aa1247f43169a03b6278671608763a8062b290aa Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:55:45 +0000 Subject: [PATCH 086/145] Fix __package__ attr and imports according to PEP366 for submodules. --- tests/test_gnupg.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 163ea33..93ba800 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -40,25 +40,41 @@ import shutil import sys import tempfile +## This is less applicable now that we're using distribute with a bootstrap +## script for newer versions of distribute, and pip>=1.3.1, since both of +## these dependencies require Python>=2.6 in order to have proper SSL support. +## ## Use unittest2 if we're on Python2.6 or less: if sys.version_info.major == 2 and sys.version_info.minor <= 6: unittest = __import__(unittest2) else: import unittest +import gnupg + +## see PEP-366 http://www.python.org/dev/peps/pep-0366/ +print("NAME: %r" % __name__) +print("PACKAGE: %r" % __package__) +if __name__ == "__main__" and __package__ is None: + __package__ = "gnupg.test" + print("NAME: %r" % __name__) + print("PACKAGE: %r" % __package__) + try: + from .. import _util + from .. import _parsers + from .. import _logger + except (ImportError, ValueError) as ierr: + print(ierr.message) -## see PEP-328 http://docs.python.org/2.5/whatsnew/pep-328.html -import .gnupg -import ._parsers -import ._util -import ._logger log = _util.log log.setLevel(9) -_here = os.path.join(os.getcwd(), 'tests') -_files = os.path.join(_here, 'files') -_tempd = os.path.join(_here, 'tmp') +print("Current directory: %s" % _util._here) +print("Current os directory: %s" % os.getcwd()) +_tests = os.path.join(os.path.join(os.getcwd(), 'gnupg'), 'test') +_files = os.path.join(_tests, 'files') +_tempd = os.path.join(_tests, 'tmp') tempfile.tempdir = _tempd if not os.path.isdir(tempfile.gettempdir()): From d81fee0912313d4b1817affd98a16c795a2b6bd4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:56:47 +0000 Subject: [PATCH 087/145] Remove the log.warn line that printed dividers between unittest runs. --- tests/test_gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 93ba800..c350bd9 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -201,7 +201,6 @@ class GPGTestCase(unittest.TestCase): else: log.warn("Can't delete homedir: '%s' not a directory" % self.homedir) - log.warn("%s%s%s" % (os.linesep, str("=" * 70), os.linesep)) def test_parsers_fix_unsafe(self): """Test that unsafe inputs are quoted out and then ignored.""" From 3233cba6bb3eeec5c1c012e9720e074c33911a99 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:58:02 +0000 Subject: [PATCH 088/145] Add a test for semicolon escapes in inputs. --- tests/test_gnupg.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index c350bd9..a17beda 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -212,6 +212,12 @@ class GPGTestCase(unittest.TestCase): has_shell = self.gpg.verify_file(test_file, fixed) self.assertFalse(has_shell.valid) + def test_parsers_fix_unsafe_semicolon(self): + """Test that we can't escape into the Python interpreter.""" + shell_input = "; import antigravity ;" + fixed = _parsers._fix_unsafe(shell_input) + print(fixed) + def test_parsers_is_hex_valid(self): """Test that valid hexidecimal passes the parsers._is_hex() check""" valid_hex = '0A6A58A14B5946ABDE18E207A3ADB67A2CDB8B35' @@ -912,14 +918,14 @@ authentication.""" suites = { 'parsers': set(['test_parsers_fix_unsafe', + 'test_parsers_fix_unsafe_semicolon', 'test_parsers_is_hex_valid', 'test_parsers_is_hex_lowercase', 'test_parsers_is_hex_invalid', 'test_copy_data_bytesio',]), 'encodings': set(['test_encodings_iso_8859_1', 'test_encodings_spiteful', - 'test_encodings_non_specified', - ]), + 'test_encodings_non_specified',]), 'basic': set(['test_homedir_creation', 'test_binary_discovery', 'test_gpg_binary', From 2825efcda51b375e8da5286190c3a7f17f936032 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 11:59:37 +0000 Subject: [PATCH 089/145] Add a unittest for GPG.recv-keys(). --- tests/test_gnupg.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index a17beda..9bcd61b 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -519,6 +519,12 @@ class GPGTestCase(unittest.TestCase): keys = self.gpg.list_keys(secret=True) self.assertTrue(os.path.isfile(self.gpg.secring)) + def test_recv_keys_default(self): + """Testing receiving keys from a keyserver.""" + key = self.gpg.recv_keys('a3adb67a2cdb8b35') + self.assertIsNotNone(key) + + def test_import_and_export(self): """Test that key import and export works.""" self.test_list_keys_initial_public() @@ -967,8 +973,9 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'keyrings': set(['test_public_keyring', 'test_secret_keyring', 'test_import_and_export', - 'test_deletion']), - 'import': set(['test_import_only']), } + 'test_deletion', + 'test_import_only', + 'test_recv_keys_default',]), } def main(args): if not args.quiet: From 1cfd8b636d305622f5a09c77001f8805d6003259 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 12:02:28 +0000 Subject: [PATCH 090/145] Add run mechanism for unittests to main module. --- src/gnupg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index cf2791f..3eade50 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1101,3 +1101,7 @@ class GPGWrapper(GPG): def is_encrypted(self, raw_data): self.is_encrypted_asym() or self.is_encrypted_sym() + +if __name__ == "__main__": + from .test import test_gnupg + test_gnupg.main() From e9498e91a9cebbc54f6f6541e3cdb4338bbd63ac Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 12:04:32 +0000 Subject: [PATCH 091/145] Fix assertions and add extra assertions to multi-recipient encryption test. --- tests/test_gnupg.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 9bcd61b..5a2e813 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -877,21 +877,31 @@ authentication.""" log.debug("encryption_decryption_multi_recipient() Ciphertext = %s" % encrypted) - self.assertNotEqual(message, encrypted) + self.assertNotEquals(message, encrypted) dec_alice = self.gpg.decrypt(encrypted, passphrase="test") - self.assertEqual(message, str(dec_alice.data)) + self.assertEquals(message, str(dec_alice.data)) dec_bob = self.gpg.decrypt(encrypted, passphrase="test") - self.assertEqual(message, str(dec_bob.data)) + self.assertEquals(message, str(dec_bob.data)) def test_symmetric_encryption_and_decryption(self): """Test symmetric encryption and decryption""" - msg = """If you have something that you don't want anyone to know, - maybe you shouldn't be doing it in the first place. - Eric Schmidt, CEO - of Google""" + msg = """If you have something that you don't want anyone to +know, maybe you shouldn't be doing it in the first place. +-- Eric Schmidt, CEO of Google""" encrypted = str(self.gpg.encrypt(msg, passphrase='quiscustodiet', symmetric=True, encrypt=False)) - decrypted = self.gpg.decrypt(encrypted, passphrase='quiscustodiet') - self.assertEqual(msg, str(decrypted.data)) + decrypt = self.gpg.decrypt(encrypted, passphrase='quiscustodiet') + decrypted = str(decrypt.data) + + log.info("Symmetrically encrypted data:\n%s" % encrypted) + log.info("Symmetrically decrypted data:\n%s" % decrypted) + + self.assertIsNotNone(encrypted) + self.assertNotEquals(encrypted, "") + self.assertNotEquals(encrypted, msg) + self.assertIsNotNone(decrypted) + self.assertNotEquals(decrypted, "") + self.assertEqual(decrypted, msg) def test_file_encryption_and_decryption(self): """Test that encryption/decryption to/from file works.""" From dfe51598cc9ddfb3df42e2591c3a4a29009e7935 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 12:07:51 +0000 Subject: [PATCH 092/145] Add script for creating a virtualenv for testing and development. --- scripts/make-dev-virtualenv.sh | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 scripts/make-dev-virtualenv.sh diff --git a/scripts/make-dev-virtualenv.sh b/scripts/make-dev-virtualenv.sh new file mode 100755 index 0000000..130626d --- /dev/null +++ b/scripts/make-dev-virtualenv.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# -*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python wrapper around GnuPG. +# Copyright © 2013 Isis Lovecruft, Andrej B. +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. + +project=python-gnupg +VENV=$(which virtualenv) +WRPR=$(which virtualenvwrapper.sh) + + +if ! test -n "$VENV" ; then + printf "Couldn't find virtualenv. Are you sure it's installed?" + exit 1 +fi + +if ! test -n "$WRPR"; then + printf "Couldn't find virtualenvwrapper. Are you sure it's installed?" + exit 1 +fi + +test -r "$WRPR" && . $WRPR +okay=$? + +if test "$okay" -eq 0 ; then + printf "Using %s as WORKON_HOME for the new virtualenv...\n" "$PWD" + printf"What should the name of the new virtualenv be? (default: '%s')\n" "$project" + read -p"Name for this virtualenv?: " name + if test -z "$name"; then + name="$project" + fi + printf "Using '$name' as our project's name..." + printf "Creating virtualenv..." + mkvirtualenv -a "$PWD" --no-site-packages \ + --distribute --prompt="(gnupg)" "$name" + exit $? +else + printf "Something went wrong..." + printf "Exit code %d from mkvirtualenv." "$okay" + exit $okay +fi From 042d52a75f7209c7dd2929bfad4873a19e9d3c22 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 21:16:19 +0000 Subject: [PATCH 093/145] Move source files to directory named for top-level package. --- {src => gnupg}/__init__.py | 0 {src => gnupg}/_ansistrm.py | 0 {src => gnupg}/_logger.py | 0 {src => gnupg}/_meta.py | 0 {src => gnupg}/_parsers.py | 0 {src => gnupg}/_util.py | 0 {src => gnupg}/_version.py | 0 {src => gnupg}/copyleft.py | 0 {src => gnupg}/gnupg.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {src => gnupg}/__init__.py (100%) rename {src => gnupg}/_ansistrm.py (100%) rename {src => gnupg}/_logger.py (100%) rename {src => gnupg}/_meta.py (100%) rename {src => gnupg}/_parsers.py (100%) rename {src => gnupg}/_util.py (100%) rename {src => gnupg}/_version.py (100%) rename {src => gnupg}/copyleft.py (100%) rename {src => gnupg}/gnupg.py (100%) diff --git a/src/__init__.py b/gnupg/__init__.py similarity index 100% rename from src/__init__.py rename to gnupg/__init__.py diff --git a/src/_ansistrm.py b/gnupg/_ansistrm.py similarity index 100% rename from src/_ansistrm.py rename to gnupg/_ansistrm.py diff --git a/src/_logger.py b/gnupg/_logger.py similarity index 100% rename from src/_logger.py rename to gnupg/_logger.py diff --git a/src/_meta.py b/gnupg/_meta.py similarity index 100% rename from src/_meta.py rename to gnupg/_meta.py diff --git a/src/_parsers.py b/gnupg/_parsers.py similarity index 100% rename from src/_parsers.py rename to gnupg/_parsers.py diff --git a/src/_util.py b/gnupg/_util.py similarity index 100% rename from src/_util.py rename to gnupg/_util.py diff --git a/src/_version.py b/gnupg/_version.py similarity index 100% rename from src/_version.py rename to gnupg/_version.py diff --git a/src/copyleft.py b/gnupg/copyleft.py similarity index 100% rename from src/copyleft.py rename to gnupg/copyleft.py diff --git a/src/gnupg.py b/gnupg/gnupg.py similarity index 100% rename from src/gnupg.py rename to gnupg/gnupg.py From 6e877d287116114c89a8c370d04738b1d41aa216 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 21:27:44 +0000 Subject: [PATCH 094/145] Fix whitespace to be a TAB instead on spaces in Makefile. *le sigh* --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e8b42e1..fdbc016 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ etags: # Sanitation targets -- clean leaves libraries, executables and tags # files, which clobber removes as well pycremoval: - find . -name '*.py[co]' -exec rm -f {} ';' + find . -name '*.py[co]' -exec rm -f {} ';' cleanup-src: pycremoval cd gnupg && rm -f \#*\# From cd319e1a934e099ebe7f694311bc1c65490f4686 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 21:28:28 +0000 Subject: [PATCH 095/145] Change license string for PyPI to be the license name, not the copyright. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b173396..0c3c86d 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ setuptools.setup( long_description = "This module allows easy access to GnuPG's key \ management, encryption and signature functionality from Python programs. \ It is intended for use with Python 2.6 or greater.", - license="""Copyright © 2013 Isis Lovecruft, et.al. see LICENSE file.""", + license="AGPLv3", version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), From 785b2760bbac133267a6ecf3ced3b6c57a09992a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 21:34:39 +0000 Subject: [PATCH 096/145] Move GPG._sign_file() to GPGBase since it's never called directly. --- gnupg/_meta.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ gnupg/gnupg.py | 44 -------------------------------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 155ff62..4f9f77b 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -538,3 +538,47 @@ class GPGBase(object): self._collect_output(proc, result) log.debug('recv_keys result: %r', result.__dict__) return result + + def _sign_file(self, file, default_key=None, passphrase=None, + clearsign=True, detach=False, binary=False): + """Create a signature for a file. + + :param file: The file stream (i.e. it's already been open()'d) to sign. + :param str keyid: The key to sign with. + :param str passphrase: The passphrase to pipe to stdin. + :param bool clearsign: If True, create a cleartext signature. + :param bool detach: If True, create a detached signature. + :param bool binary: If True, do not ascii armour the output. + """ + log.debug("_sign_file():") + if binary: + log.info("Creating binary signature for file %s" % file) + args = ['--sign'] + else: + log.info("Creating ascii-armoured signature for file %s" % file) + args = ['--sign --armor'] + + if clearsign: + args.append("--clearsign") + if detach: + log.warn("Cannot use both --clearsign and --detach-sign.") + log.warn("Using default GPG behaviour: --clearsign only.") + elif detach and not clearsign: + args.append("--detach-sign") + + if default_key: + args.append(str("--default-key %s" % default_key)) + + ## We could use _handle_io here except for the fact that if the + ## passphrase is bad, gpg bails and you can't write the message. + result = self._result_map['sign'](self) + proc = self._open_subprocess(args, passphrase is not None) + try: + if passphrase: + _util._write_passphrase(proc.stdin, passphrase, self._encoding) + writer = _util._threaded_copy_data(file, proc.stdin) + except IOError as ioe: + log.exception("Error writing message: %s" % ioe.message) + writer = None + self._collect_output(proc, result, writer, proc.stdin) + return result diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 3eade50..d0b3a22 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -195,50 +195,6 @@ class GPG(GPGBase): result = None return result - def _sign_file(self, file, default_key=None, passphrase=None, - clearsign=True, detach=False, binary=False): - """Create a signature for a file. - - :param file: The file stream (i.e. it's already been open()'d) to sign. - :param str keyid: The key to sign with. - :param str passphrase: The passphrase to pipe to stdin. - :param bool clearsign: If True, create a cleartext signature. - :param bool detach: If True, create a detached signature. - :param bool binary: If True, do not ascii armour the output. - """ - log.debug("_sign_file():") - if binary: - log.info("Creating binary signature for file %s" % file) - args = ['--sign'] - else: - log.info("Creating ascii-armoured signature for file %s" % file) - args = ['--sign --armor'] - - if clearsign: - args.append("--clearsign") - if detach: - log.warn("Cannot use both --clearsign and --detach-sign.") - log.warn("Using default GPG behaviour: --clearsign only.") - elif detach and not clearsign: - args.append("--detach-sign") - - if default_key: - args.append(str("--default-key %s" % default_key)) - - ## We could use _handle_io here except for the fact that if the - ## passphrase is bad, gpg bails and you can't write the message. - result = self._result_map['sign'](self) - proc = self._open_subprocess(args, passphrase is not None) - try: - if passphrase: - _util._write_passphrase(proc.stdin, passphrase, self._encoding) - writer = _util._threaded_copy_data(file, proc.stdin) - except IOError as ioe: - log.exception("Error writing message: %s" % ioe.message) - writer = None - self._collect_output(proc, result, writer, proc.stdin) - return result - def verify(self, data): """Verify the signature on the contents of the string ``data``. From d75800a7e020fbaa03fbd4b1c41e76a2d424e75f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 3 Jun 2013 21:35:23 +0000 Subject: [PATCH 097/145] Add tests package __init__.py. --- tests/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..2fe409b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# +# This file is part of python-gnupg, a Python interface to GnuPG. +# Copyright © 2013 Isis Lovecruft +# © 2008-2012 Vinay Sajip +# © 2005 Steve Traugott +# © 2004 A.M. Kuchling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +"""test/__init__.py +------------------- +Initialisation file for gnupg._test unittesting package. +""" From 295d98fbdc6a9bff927da8fe011241d7bbbd8a3c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 4 Jun 2013 01:42:07 +0000 Subject: [PATCH 098/145] =?UTF-8?q?GPG.encrypt=5Ffile()=E2=86=92GPGBase.en?= =?UTF-8?q?crypt()=20and=20fix=20doctests=20and=20documentation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gnupg/_meta.py | 157 +++++++++++++++++++++++++++++++++++++++++++ gnupg/gnupg.py | 179 +++++++++++++++++-------------------------------- 2 files changed, 220 insertions(+), 116 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 4f9f77b..530064d 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -582,3 +582,160 @@ class GPGBase(object): writer = None self._collect_output(proc, result, writer, proc.stdin) return result + + def _encrypt(self, data, recipients, + default_key=None, + passphrase=None, + armor=True, + encrypt=True, + symmetric=False, + always_trust=True, + output=None, + cipher_algo='AES256', + digest_algo='SHA512', + compress_algo='ZLIB'): + """Encrypt the message read from the file-like object ``data``. + + :param str data: The file or bytestream to encrypt. + + :param str recipients: The recipients to encrypt to. Recipients must + be specified keyID/fingerprint. Care should be taken in Python2.x + to make sure that the given fingerprint is in fact a string and + not a unicode object. + + :param str default_key: The keyID/fingerprint of the key to use for + signing. If given, ``data`` will be encrypted and signed. + + :param str passphrase: If given, and ``default_key`` is also given, + use this passphrase to unlock the secret portion of the + ``default_key`` to sign the encrypted ``data``. Otherwise, if + ``default_key`` is not given, but ``symmetric=True``, then use + this passphrase as the passphrase for symmetric + encryption. Signing and symmetric encryption should *not* be + combined when sending the ``data`` to other recipients, else the + passphrase to the secret key would be shared with them. + + :param bool armor: If True, ascii armor the output; otherwise, the + output will be in binary format. (Default: True) + + :param bool encrypt: If True, encrypt the ``data`` using the + ``recipients`` public keys. (Default: True) + + :param bool symmetric: If True, encrypt the ``data`` to ``recipients`` + using a symmetric key. See the ``passphrase`` parameter. Symmetric + encryption and public key encryption can be used simultaneously, + and will result in a ciphertext which is decryptable with either + the symmetric ``passphrase`` or one of the corresponding private + keys. + + :param bool always_trust: If True, ignore trust warnings on recipient + keys. If False, display trust warnings. (default: True) + + :param str output: The output file to write to. If not specified, the + encrypted output is returned, and thus should be stored as an + object in Python. For example: + + >>> import shutil + >>> import gnupg + >>> if os.path.exists("doctests"): + ... shutil.rmtree("doctests") + >>> gpg = gnupg.GPG(homedir="doctests") + >>> key_settings = gpg.gen_key_input(key_type='RSA', + ... key_length=1024, + ... key_usage='ESCA', + ... passphrase='foo') + >>> key = gpg.gen_key(key_settings) + >>> message = "The crow flies at midnight." + >>> encrypted = str(gpg.encrypt(message, key.printprint)) + >>> assert encrypted != message + >>> assert not encrypted.isspace() + >>> decrypted = str(gpg.decrypt(encrypted)) + >>> assert not decrypted.isspace() + >>> decrypted + 'The crow flies at midnight.' + + :param str cipher_algo: The cipher algorithm to use. To see available + algorithms with your version of GnuPG, do: + ``$ gpg --with-colons --list-config ciphername``. + The default ``cipher_algo``, if unspecified, is ``'AES256'``. + + :param str digest_algo: The hash digest to use. Again, to see which + hashes your GnuPG is capable of using, do: + ``$ gpg --with-colons --list-config digestname``. + The default, if unspecified, is ``'SHA512'``. + + :param str compress_algo: The compression algorithm to use. Can be one + of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or ``'Uncompressed'``. + """ + args = [] + + if output: + if getattr(output, 'fileno', None) is not None: + ## avoid overwrite confirmation message + if getattr(output, 'name', None) is None: + if os.path.exists(output): + os.remove(output) + args.append('--output %s' % output) + else: + if os.path.exists(output.name): + os.remove(output.name) + args.append('--output %s' % output.name) + + if armor: args.append('--armor') + if always_trust: args.append('--always-trust') + if cipher_algo: args.append('--cipher-algo %s' % cipher_algo) + if compress_algo: args.append('--compress-algo %s' % compress_algo) + + if default_key: + args.append('--sign') + args.append('--default-key %s' % default_key) + if digest_algo: + args.append('--digest-algo %s' % digest_algo) + + ## both can be used at the same time for an encrypted file which + ## is decryptable with a passphrase or secretkey. + if symmetric: args.append('--symmetric') + if encrypt: args.append('--encrypt') + + if len(recipients) >= 1: + log.debug("GPG.encrypt() called for recipients '%s' with type '%s'" + % (recipients, type(recipients))) + + if isinstance(recipients, (list, tuple)): + for recp in recipients: + if not _util._py3k: + if isinstance(recp, unicode): + try: + assert _parsers._is_hex(str(recp)) + except AssertionError: + log.info("Can't accept recipient string: %s" + % recp) + else: + args.append('--recipient %s' % str(recp)) + continue + ## will give unicode in 2.x as '\uXXXX\uXXXX' + args.append('--recipient %r' % recp) + continue + if isinstance(recp, str): + args.append('--recipient %s' % recp) + + elif (not _util._py3k) and isinstance(recp, basestring): + for recp in recipients.split('\x20'): + args.append('--recipient %s' % recp) + + elif _util._py3k and isinstance(recp, str): + for recp in recipients.split(' '): + args.append('--recipient %s' % recp) + ## ...and now that we've proven py3k is better... + + else: + log.debug("Don't know what to do with recipients: '%s'" + % recipients) + + result = self._result_map['crypt'](self) + log.debug("Got data '%s' with type '%s'." + % (data, type(data))) + self._handle_io(args, data, result, + passphrase=passphrase, binary=True) + log.debug('GPG.encrypt_file(): Result: %r', result.data) + return result diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index d0b3a22..929a9ce 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -831,135 +831,82 @@ generate keys. Please see return out - def encrypt_file(self, filename, recipients, - default_key=None, - passphrase=None, - armor=True, - encrypt=True, - symmetric=False, - always_trust=True, - output=None, - cipher_algo='AES256', - digest_algo='SHA512', - compress_algo='ZLIB'): - """Encrypt the message read from the file-like object ``filename``. - - :param str filename: The file or bytestream to encrypt. - :param str recipients: The recipients to encrypt to. Recipients must - be specified keyID/fingerprint. Care should be - taken in Python2.x to make sure that the given - fingerprint is in fact a string and not a - unicode object. - :param str default_key: The keyID/fingerprint of the key to use for - signing. If given, ``filename`` will be - encrypted and signed. - :param bool always_trust: If True, ignore trust warnings on recipient - keys. If False, display trust warnings. - (default: True) - :param str passphrase: If True, use this passphrase to unlock the - secret portion of the ``default_key`` for - signing. - :param bool armor: If True, ascii armor the output; otherwise, the - output will be in binary format. (default: True) - :param str output: The output file to write to. If not specified, the - encrypted output is returned, and thus should be - stored as an object in Python. For example: - """ - args = [] - - if output: - if getattr(output, 'fileno', None) is not None: - ## avoid overwrite confirmation message - if getattr(output, 'name', None) is None: - if os.path.exists(output): - os.remove(output) - args.append('--output %s' % output) - else: - if os.path.exists(output.name): - os.remove(output.name) - args.append('--output %s' % output.name) - - if armor: args.append('--armor') - if always_trust: args.append('--always-trust') - if cipher_algo: args.append('--cipher-algo %s' % cipher_algo) - if compress_algo: args.append('--compress-algo %s' % compress_algo) - - if default_key: - args.append('--sign') - args.append('--default-key %s' % default_key) - if digest_algo: - args.append('--digest-algo %s' % digest_algo) - - ## both can be used at the same time for an encrypted file which - ## is decryptable with a passphrase or secretkey. - if symmetric: args.append('--symmetric') - if encrypt: args.append('--encrypt') - - if len(recipients) >= 1: - log.debug("GPG.encrypt() called for recipients '%s' with type '%s'" - % (recipients, type(recipients))) - - if isinstance(recipients, (list, tuple)): - for recp in recipients: - if not _util._py3k: - if isinstance(recp, unicode): - try: - assert _parsers._is_hex(str(recp)) - except AssertionError: - log.info("Can't accept recipient string: %s" - % recp) - else: - args.append('--recipient %s' % str(recp)) - continue - ## will give unicode in 2.x as '\uXXXX\uXXXX' - args.append('--recipient %r' % recp) - continue - if isinstance(recp, str): - args.append('--recipient %s' % recp) - - elif (not _util._py3k) and isinstance(recp, basestring): - for recp in recipients.split('\x20'): - args.append('--recipient %s' % recp) - - elif _util._py3k and isinstance(recp, str): - for recp in recipients.split(' '): - args.append('--recipient %s' % recp) - ## ...and now that we've proven py3k is better... - - else: - log.debug("Don't know what to do with recipients: '%s'" - % recipients) - - result = self._result_map['crypt'](self) - log.debug("Got filename '%s' with type '%s'." - % (filename, type(filename))) - self._handle_io(args, filename, result, - passphrase=passphrase, binary=True) - log.debug('GPG.encrypt_file(): Result: %r', result.data) - return result - def encrypt(self, data, *recipients, **kwargs): """Encrypt the message contained in ``data`` to ``recipients``. + :param str data: The file or bytestream to encrypt. + + :param str recipients: The recipients to encrypt to. Recipients must + be specified keyID/fingerprint. Care should be taken in Python2.x + to make sure that the given fingerprint is in fact a string and + not a unicode object. + + :param str default_key: The keyID/fingerprint of the key to use for + signing. If given, ``data`` will be encrypted and signed. + + :param str passphrase: If given, and ``default_key`` is also given, + use this passphrase to unlock the secret portion of the + ``default_key`` to sign the encrypted ``data``. Otherwise, if + ``default_key`` is not given, but ``symmetric=True``, then use + this passphrase as the passphrase for symmetric + encryption. Signing and symmetric encryption should *not* be + combined when sending the ``data`` to other recipients, else the + passphrase to the secret key would be shared with them. + + :param bool armor: If True, ascii armor the output; otherwise, the + output will be in binary format. (Default: True) + + :param bool encrypt: If True, encrypt the ``data`` using the + ``recipients`` public keys. (Default: True) + + :param bool symmetric: If True, encrypt the ``data`` to ``recipients`` + using a symmetric key. See the ``passphrase`` parameter. Symmetric + encryption and public key encryption can be used simultaneously, + and will result in a ciphertext which is decryptable with either + the symmetric ``passphrase`` or one of the corresponding private + keys. + + :param bool always_trust: If True, ignore trust warnings on recipient + keys. If False, display trust warnings. (default: True) + + :param str output: The output file to write to. If not specified, the + encrypted output is returned, and thus should be stored as an + object in Python. For example: + >>> import shutil + >>> import gnupg >>> if os.path.exists("doctests"): ... shutil.rmtree("doctests") - >>> gpg = GPG(homedir="doctests") + >>> gpg = gnupg.GPG(homedir="doctests") >>> key_settings = gpg.gen_key_input(key_type='RSA', ... key_length=1024, ... key_usage='ESCA', ... passphrase='foo') >>> key = gpg.gen_key(key_settings) - >>> encrypted = gpg.encrypt("sekrit", key.printprint) - >>> message = str(encrypted) - >>> assert message != 'sekrit' - >>> decrypted = gpg.decrypt(message) - >>> assert decrypted - >>> str(decrypted) - 'sekrit' + >>> message = "The crow flies at midnight." + >>> encrypted = str(gpg.encrypt(message, key.printprint)) + >>> assert encrypted != message + >>> assert not encrypted.isspace() + >>> decrypted = str(gpg.decrypt(encrypted)) + >>> assert not decrypted.isspace() + >>> decrypted + 'The crow flies at midnight.' + + :param str cipher_algo: The cipher algorithm to use. To see available + algorithms with your version of GnuPG, do: + ``$ gpg --with-colons --list-config ciphername``. + The default ``cipher_algo``, if unspecified, is ``'AES256'``. + + :param str digest_algo: The hash digest to use. Again, to see which + hashes your GnuPG is capable of using, do: + ``$ gpg --with-colons --list-config digestname``. + The default, if unspecified, is ``'SHA512'``. + + :param str compress_algo: The compression algorithm to use. Can be one + of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or ``'Uncompressed'``. """ stream = _make_binary_stream(data, self._encoding) - result = self.encrypt_file(stream, recipients, **kwargs) + result = self._encrypt(stream, recipients, **kwargs) stream.close() return result From e7f189983297f867ef0e7ed597e85a2a041d55db Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 8 Jun 2013 11:19:08 +0000 Subject: [PATCH 099/145] Fix the hash bang python in scripts/distribute_setup.py. --- scripts/distribute_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/distribute_setup.py b/scripts/distribute_setup.py index dc461f3..6899f83 100755 --- a/scripts/distribute_setup.py +++ b/scripts/distribute_setup.py @@ -1,4 +1,4 @@ -#!python +#!/usr/bin/env python """Bootstrap distribute installation If you want to use setuptools in your package's setup.py, just include this From 744d5c64d310418646758f7d5b7f4441e6746248 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 04:27:52 +0000 Subject: [PATCH 100/145] Explicitly specify package dirs and names in setup.py. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c3c86d..d1a76f9 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,8 @@ It is intended for use with Python 2.6 or greater.", maintainer_email=__contact__, url=__url__, - packages=setuptools.find_packages(), + package_dir={'gnupg': 'gnupg'}, + packages=['gnupg'], install_requires=reqs, dependency_links=deps, From 9f47765a3f61d84c93263721d79205eb9d136e7d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 05:40:55 +0000 Subject: [PATCH 101/145] Fix print statements in scripts/make-dev-virtualenv.sh script. --- scripts/make-dev-virtualenv.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/make-dev-virtualenv.sh b/scripts/make-dev-virtualenv.sh index 130626d..cd3a128 100755 --- a/scripts/make-dev-virtualenv.sh +++ b/scripts/make-dev-virtualenv.sh @@ -36,8 +36,8 @@ test -r "$WRPR" && . $WRPR okay=$? if test "$okay" -eq 0 ; then - printf "Using %s as WORKON_HOME for the new virtualenv...\n" "$PWD" - printf"What should the name of the new virtualenv be? (default: '%s')\n" "$project" + printf "Using %s as WORKON_HOME for the new virtualenv...\n" $PWD + printf "What should the name of the new virtualenv be? (default: '%s')\n" $project read -p"Name for this virtualenv?: " name if test -z "$name"; then name="$project" From b0001ef94c80bd07b4a727128393532bd7535e1d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 05:41:25 +0000 Subject: [PATCH 102/145] Make sure setuptools/distribute is present in new dev virtualenvs. --- scripts/make-dev-virtualenv.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/make-dev-virtualenv.sh b/scripts/make-dev-virtualenv.sh index cd3a128..adf8c76 100755 --- a/scripts/make-dev-virtualenv.sh +++ b/scripts/make-dev-virtualenv.sh @@ -44,7 +44,7 @@ if test "$okay" -eq 0 ; then fi printf "Using '$name' as our project's name..." printf "Creating virtualenv..." - mkvirtualenv -a "$PWD" --no-site-packages \ + mkvirtualenv -a "$PWD" --no-site-packages --unzip-setuptools \ --distribute --prompt="(gnupg)" "$name" exit $? else From 900c41619b81c98927feb7460a2537ca712683d1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 05:44:43 +0000 Subject: [PATCH 103/145] If using a virtualenv, we don't want the env pip and distribute to be upgraded. * The proper way to do this is to upgrade globally with the get_pip.py script and the distribute_setup.py script, and then create the virtualenv with: $ . $(which virtualenvwrapper.sh) $ WORKON_HOME=~/.virtualenvs && export WORKON_HOME $ mkvirtualenv -a $PWD -r requirements.txt --unzip-setuptools \ --distribute --no-site-packages gnupg --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 15d3051..9486d49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ # Also, note that SSL package delivery is *not* entirely fixed yet. See # https://github.com/TheTorProject/ooni-backend/pull/1#discussion_r4084881 # -pip>=1.3.1 +#pip>=1.3.1 # # NOTE: setuptools is currently (as of 27 May 2013) being merged back into its # parent project, distribute. By using the included distribute_setup.py @@ -45,7 +45,7 @@ pip>=1.3.1 # back into its parent project, distribute. Also, the only way to package for # both Python 2 and 3 is to use distribute. # -distribute>=0.6.45 +#distribute>=0.6.45 # # Sphinx is only necessary for building documentation, so it is added in # setup.py under extras_require['docs']. From 762f750db0c48d36b86464789da783f27df68e00 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:07:00 +0000 Subject: [PATCH 104/145] Should be TestCase.assertEquals to test equality, not equivalence. --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 5a2e813..a25d41d 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -724,7 +724,7 @@ authentication.""" encrypted = str(gpg.encrypt(message, dijk)) log.debug("Plaintext: %s" % message) log.debug("Encrypted: %s" % encrypted) - self.assertNotEqual(message, encrypted) + self.assertNotEquals(message, encrypted) def test_encryption_alt_encoding(self): """Test encryption with latin-1 encoding""" From 9f6a0a08fbe908f154514dc6c81ae66487dbfdc4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:09:34 +0000 Subject: [PATCH 105/145] Use separate keyrings during multi recipient test to avoid overwrites. --- tests/test_gnupg.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index a25d41d..f5ed05f 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -774,14 +774,15 @@ authentication.""" 'passphrase': 'overalls' } ian_input = self.gpg.gen_key_input(separate_keyring=True, **ian) + log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) ian_key = self.gpg.gen_key(ian_input) - log.debug("ian_key status: %s" % ian_key.status) ian_fpr = str(ian_key.fingerprint) kat_input = self.gpg.gen_key_input(separate_keyring=True, **kat) + log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) kat_key = self.gpg.gen_key(kat_input) - log.debug("kat_key status: %s" % kat_key.status) kat_fpr = str(kat_key.fingerprint) + self.gpg.import_keys(kat_key.data) message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic @@ -794,11 +795,13 @@ authentication.""" log.debug("kat_fpr type: %s" % type(kat_fpr)) log.debug("ian_fpr type: %s" % type(ian_fpr)) - encrypted = self.gpg.encrypt(message, (ian_fpr, kat_fpr)) + encrypted = str(self.gpg.encrypt(message, ian_fpr, kat_fpr)) log.debug("Plaintext: %s" % message) - log.debug("Ciphertext: %s" % str(encrypted.data)) + log.debug("Ciphertext: %s" % encrypted) - self.assertNotEqual(message, str(encrypted.data)) + self.assertNotEquals(message, encrypted) + self.assertIsNotNone(encrypted) + self.assertGreater(len(encrypted), 0) def test_decryption(self): """Test decryption""" From ab00cd69e40c350085e8968dfc066d85c4e76cb3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:11:33 +0000 Subject: [PATCH 106/145] Import with from the future. --- tests/test_gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index f5ed05f..034363d 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -23,6 +23,7 @@ A test harness and unittests for gnupg.py. from __future__ import absolute_import from __future__ import print_function +from __future__ import with_statement from argparse import ArgumentParser from codecs import open as open from functools import wraps From 61c9a8818d73373ad4d76f32d2318cfbfe4bc8b9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:12:46 +0000 Subject: [PATCH 107/145] Rename a unittest to test what it actually tests. --- tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 034363d..fb40c96 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -240,7 +240,7 @@ class GPGTestCase(unittest.TestCase): with self.assertRaises(LookupError): _util.find_encodings(enc) - def test_encodings_iso_8859_1(self): + def test_encodings_big5(self): """Test that _util.find_encodings works for Chinese Traditional.""" enc = 'big5' coder = _util.find_encodings(enc) @@ -943,7 +943,7 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_lowercase', 'test_parsers_is_hex_invalid', 'test_copy_data_bytesio',]), - 'encodings': set(['test_encodings_iso_8859_1', + 'encodings': set(['test_encodings_big5', 'test_encodings_spiteful', 'test_encodings_non_specified',]), 'basic': set(['test_homedir_creation', From 59de070468fe79a9803e7b094d7f72a457325f89 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:14:05 +0000 Subject: [PATCH 108/145] Write the recv key to a file for later tests to use to save entropy. --- tests/test_gnupg.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index fb40c96..6c890d0 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -522,9 +522,15 @@ class GPGTestCase(unittest.TestCase): def test_recv_keys_default(self): """Testing receiving keys from a keyserver.""" - key = self.gpg.recv_keys('a3adb67a2cdb8b35') + fpr = '0A6A58A14B5946ABDE18E207A3ADB67A2CDB8B35' + key = self.gpg.recv_keys(fpr) self.assertIsNotNone(key) - + self.assertNotEquals(key, "") + self.assertGreater(len(str(key)), 0) + keyfile = os.path.join(self.gpg._keys_dir, 'test_key_3.pub') + with open(keyfile, 'w') as fh: + fh.write(key) + self.assertTrue(os.path.isfile(keyfile)) def test_import_and_export(self): """Test that key import and export works.""" From c7f648e2a81c0f29a1b8bdac8ab439f01eb0c24f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:23:09 +0000 Subject: [PATCH 109/145] Combine sig encoding and algorithm tests into string test to save entropy. --- tests/test_gnupg.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 6c890d0..3ce48ce 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -580,7 +580,7 @@ class GPGTestCase(unittest.TestCase): log.info("now: %r", ascii) self.assertEqual(0, match, "Keys must match") - def test_signature_string(self): + def test_signature_string_algorithm_encoding(self): """Test that signing a message string works.""" key = self.generate_key("Werner Koch", "gnupg.org") message = "Damn, I really wish GnuPG had ECC support." @@ -588,16 +588,28 @@ class GPGTestCase(unittest.TestCase): passphrase='wernerkoch') print("SIGNATURE:\n", sig.data) self.assertIsNotNone(sig.data) - - def test_signature_algorithm(self): - """Test that determining the signing algorithm works.""" - key = self.generate_key("Ron Rivest", "rsa.com") - message = "Someone should add GCM block cipher mode to PyCrypto." - sig = self.gpg.sign(message, default_key=key.fingerprint, - passphrase='ronrivest') print("ALGORITHM:\n", sig.sig_algo) self.assertIsNotNone(sig.sig_algo) + log.info("Testing signature strings with alternate encodings.") + self.gpg._encoding = 'latin-1' + message = "Mêle-toi de tes oignons" + sig = self.gpg.sign(message, default_key=key.fingerprint, + passphrase='wernerkoch') + self.assertTrue(sig) + print("SIGNATURE:\n", sig.data) + self.assertIsNotNone(sig.data) + print("ALGORITHM:\n", sig.sig_algo) + self.assertIsNotNone(sig.sig_algo) + + fpr = str(key.fingerprint) + seckey = self.gpg.export_keys(fpr, secret=True, subkeys=True) + keyfile = os.path.join(_files, 'test_key_4.sec') + log.info("Writing generated key to %s" % keyfile) + with open(keyfile, 'w') as fh: + fh.write(seckey) + self.assertTrue(os.path.isfile(keyfile)) + def test_signature_string_bad_passphrase(self): """Test that signing and verification works.""" key = self.generate_key("Taher ElGamal", "cryto.me") @@ -606,14 +618,6 @@ class GPGTestCase(unittest.TestCase): passphrase='foo') self.assertFalse(sig, "Bad passphrase should fail") - def test_signature_string_alternate_encoding(self): - key = self.generate_key("Nos Oignons", "nos-oignons.net") - self.gpg._encoding = 'latin-1' - message = "Mêle-toi de tes oignons" - sig = self.gpg.sign(message, default_key=key.fingerprint, - passphrase='nosoignons') - self.assertTrue(sig) - def test_signature_file(self): """Test that signing a message file works.""" key = self.generate_key("Leonard Adleman", "rsa.com") From 5e47dd7351e95837fad1316294eac992b1a07250 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:24:46 +0000 Subject: [PATCH 110/145] Use one of our test_key files for a sig test to save entropy. --- tests/test_gnupg.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 3ce48ce..d42600d 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -612,10 +612,13 @@ class GPGTestCase(unittest.TestCase): def test_signature_string_bad_passphrase(self): """Test that signing and verification works.""" - key = self.generate_key("Taher ElGamal", "cryto.me") + keyfile = os.path.join(_files, 'test_key_1.sec') + key = open(keyfile).read() + self.gpg.import_keys(key) + key = self.gpg.list_keys()[0] + fpr = key['fingerprint'] message = 'أصحاب المصالح لا يحبون الثوراتز' - sig = self.gpg.sign(message, default_key=key.fingerprint, - passphrase='foo') + sig = self.gpg.sign(message, default_key=fpr, passphrase='foo') self.assertFalse(sig, "Bad passphrase should fail") def test_signature_file(self): From 1afcda836472e941dbc7f7ef148fcc504f49b35b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:25:34 +0000 Subject: [PATCH 111/145] Fix a misnamed process thread variable. --- gnupg/gnupg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 929a9ce..a7cb39b 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -468,9 +468,8 @@ class GPG(GPGBase): args.append(key) proc = self._open_subprocess(args) - result = self._result_map['list'](self) - self._collect_output(proc, result, stdin=p.stdin) + self._collect_output(proc, result, stdin=proc.stdin) return result def gen_key(self, input): From ceb38fdefef1740bdc0abedd1ad05619a77526c2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:33:30 +0000 Subject: [PATCH 112/145] Change directory shortcuts in gnupg._util.py module. --- gnupg/_util.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index e225838..213c564 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -22,8 +22,6 @@ Extra utilities for python-gnupg. from __future__ import absolute_import from datetime import datetime -from inspect import currentframe -from inspect import getabsfile from socket import gethostname from time import gmtime from time import mktime @@ -52,15 +50,16 @@ try: isinstance(__name__, basestring) except NameError: msg = "Sorry, python-gnupg requires a Python version with proper" - msg += " unicode support. Please upgrade to Python>=2.3." + msg += " unicode support. Please upgrade to Python>=2.6." raise SystemExit(msg) except NameError: _py3k = True ## Directory shortcuts: -#_here = os.getcwd() -_here = getabsfile(currentframe()).rsplit(os.path.sep, 1)[0] ## current dir +## we don't want to use this one because it writes to the install dir: +#_here = getabsfile(currentframe()).rsplit(os.path.sep, 1)[0] +_here = os.path.join(os.getcwd(), 'gnupg') ## current dir _test = os.path.join(os.path.join(_here, 'test'), 'tmp') ## ./tests/tmp _user = os.environ.get('HOME') ## $HOME _ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg From a707a50c31826e29f8b8898f562ab0ee84013c02 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:34:15 +0000 Subject: [PATCH 113/145] Update test directory shortcut in gnupg/_logger.py. --- gnupg/_logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/_logger.py b/gnupg/_logger.py index fc241bb..fca1182 100644 --- a/gnupg/_logger.py +++ b/gnupg/_logger.py @@ -61,7 +61,7 @@ def create_logger(level=logging.NOTSET): 40 ERROR Error messages and tracebacks. 50 CRITICAL Unhandled exceptions and tracebacks. """ - _test = os.path.join(os.getcwd(), 'tests') + _test = os.path.join(os.path.join(os.getcwd(), 'gnupg'), 'test') _now = datetime.now().strftime("%Y-%m-%d_%H%M%S") _fn = os.path.join(_test, "%s_test_gnupg.log" % _now) _fmt = "%(relativeCreated)-4d L%(lineno)-4d:%(funcName)-18.18s %(levelname)-7.7s %(message)s" From bcaa26685b0d59b659912150012a8f9f9cae40b5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:35:17 +0000 Subject: [PATCH 114/145] Change several imports for installed modules to be absolute in _meta.py. --- gnupg/_meta.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 530064d..d58d174 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -18,10 +18,6 @@ from __future__ import absolute_import -from psutil import process_iter -from subprocess import Popen -from subprocess import PIPE -from threading import Thread import atexit import codecs @@ -31,7 +27,10 @@ import encodings ## See https://code.patternsinthevoid.net/?p=android-locale-hack.git import locale import os +import psutil +import subprocess import sys +import threading from . import _parsers from . import _util @@ -39,7 +38,6 @@ from . import _util from ._parsers import _check_preferences from ._parsers import _sanitise_list from ._util import log -from ._util import _conf class GPGMeta(type): @@ -72,7 +70,7 @@ class GPGMeta(type): returns None. """ identity = os.getresuid() - for proc in process_iter(): + for proc in psutil.process_iter(): if (proc.name == "gpg-agent") and proc.is_running: log.debug("Found gpg-agent process with pid %d" % proc.pid) if proc.uids == identity: @@ -102,7 +100,7 @@ class GPGBase(object): verbose=False, options=None): self.binary = _util._find_binary(binary) - self.homedir = home if home else _conf + self.homedir = home if home else _util._conf pub = _parsers._fix_unsafe(keyring) if keyring else 'pubring.gpg' sec = _parsers._fix_unsafe(secring) if secring else 'secring.gpg' self.keyring = os.path.join(self._homedir, pub) @@ -319,8 +317,8 @@ class GPGBase(object): """ if not directory: log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'" - % _conf) - directory = _conf + % _util._conf) + directory = _util._conf hd = _parsers._fix_unsafe(directory) log.debug("GPGBase._homedir_setter(): got directory '%s'" % hd) @@ -416,7 +414,8 @@ class GPGBase(object): ## -argument-sequence-to-a-string-on-windows cmd = ' '.join(self._make_args(args, passphrase)) log.debug("Sending command to GnuPG process:%s%s" % (os.linesep, cmd)) - return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) + return subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) def _read_response(self, stream, result): """Reads all the stderr output from GPG, taking notice only of lines @@ -481,13 +480,14 @@ class GPGBase(object): close it before returning. """ stderr = codecs.getreader(self._encoding)(process.stderr) - rr = Thread(target=self._read_response, args=(stderr, result)) + rr = threading.Thread(target=self._read_response, + args=(stderr, result)) rr.setDaemon(True) log.debug('stderr reader: %r', rr) rr.start() stdout = process.stdout - dr = Thread(target=self._read_data, args=(stdout, result)) + dr = threading.Thread(target=self._read_data, args=(stdout, result)) dr.setDaemon(True) log.debug('stdout reader: %r', dr) dr.start() From e1139c7e371311a1d1adf8632b2b9c1b94189e93 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 08:36:22 +0000 Subject: [PATCH 115/145] Remove commented out code from _util._copy_data(). --- gnupg/_util.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index 213c564..3cb6342 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -123,14 +123,6 @@ def _copy_data(instream, outstream): """ sent = 0 - #try: - # #assert (util._is_stream(instream) - # # or isinstance(instream, file)), "instream not stream or file" - # assert isinstance(outstream, file), "outstream is not a file" - #except AssertionError as ae: - # log.exception(ae) - # return - coder = find_encodings() while True: From a37557c00086b995204bfee648848c282707f8f7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 9 Jun 2013 11:22:42 +0000 Subject: [PATCH 116/145] Change the sets of unittests to use the combined sig test. --- tests/test_gnupg.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index d42600d..0cb983c 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -985,10 +985,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_verification_detached_binary', 'test_signature_file', 'test_signature_string_bad_passphrase', - 'test_signature_string_alternate_encoding', 'test_signature_string_verification', - 'test_signature_algorithm', - 'test_signature_string']), + 'test_signature_string_algorithm_encoding']), 'crypt': set(['test_encryption', 'test_encryption_alt_encoding', 'test_encryption_multi_recipient', From 1e030112969db84550ccd5037ea6c162a78cdb54 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 01:42:05 +0000 Subject: [PATCH 117/145] Remove unnecessary comment, there is now a unittest for this condition. --- gnupg/_parsers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 3644094..428cb8d 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -118,7 +118,6 @@ def _fix_unsafe(shell_input): :param str shell_input: The input intended for the GnuPG process. """ - ## xxx do we want to add ';'? _unsafe = re.compile(r'[^\w@%+=:,./-]', 256) try: if len(_unsafe.findall(shell_input)) == 0: From 9b9e441dd78596a7189bbd15269c03986be5619f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 01:44:04 +0000 Subject: [PATCH 118/145] Update function docs for _parsers._is_allowed() function. --- gnupg/_parsers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 428cb8d..43edbf4 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -142,21 +142,22 @@ def _hyphenate(input, add_prefix=False): return ret def _is_allowed(input): - """ - Check that an option or argument given to GPG is in the set of allowed + """Check that an option or argument given to GPG is in the set of allowed options, the latter being a strict subset of the set of all options known to GPG. :param str input: An input meant to be parsed as an option or flag to the GnuPG process. Should be formatted the same as an option or flag to the commandline gpg, i.e. "--encrypt-files". - :ivar frozenset _possible: All known GPG options and flags. - :ivar frozenset _allowed: All allowed GPG options and flags, e.g. all GPG - options and flags which we are willing to - acknowledge and parse. If we want to support a - new option, it will need to have its own parsing - class and its name will need to be added to this - set. + + :ivar frozenset gnupg_options: All known GPG options and flags. + + :ivar frozenset allowed: All allowed GPG options and flags, e.g. all GPG + options and flags which we are willing to + acknowledge and parse. If we want to support a + new option, it will need to have its own parsing + class and its name will need to be added to this + set. :rtype: Exception or str :raise: :exc:UsageError if ``_allowed`` is not a subset of ``_possible``. From 4871e9e038f20fc296f08ea3dfa7c9b7c3abdc07 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 02:28:29 +0000 Subject: [PATCH 119/145] Move allowed_options into separate groups in their own functions. --- gnupg/_parsers.py | 564 ++++++++++++++++++++++++++-------------------- 1 file changed, 316 insertions(+), 248 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 43edbf4..6ea4963 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -165,261 +165,19 @@ def _is_allowed(input): :return: The original parameter ``input``, unmodified and unsanitized, if no errors occur. """ - - three_hundred_eighteen = (""" ---allow-freeform-uid --multifile ---allow-multiple-messages --no ---allow-multisig-verification --no-allow-freeform-uid ---allow-non-selfsigned-uid --no-allow-multiple-messages ---allow-secret-key-import --no-allow-non-selfsigned-uid ---always-trust --no-armor ---armor --no-armour ---armour --no-ask-cert-expire ---ask-cert-expire --no-ask-cert-level ---ask-cert-level --no-ask-sig-expire ---ask-sig-expire --no-auto-check-trustdb ---attribute-fd --no-auto-key-locate ---attribute-file --no-auto-key-retrieve ---auto-check-trustdb --no-batch ---auto-key-locate --no-comments ---auto-key-retrieve --no-default-keyring ---batch --no-default-recipient ---bzip2-compress-level --no-disable-mdc ---bzip2-decompress-lowmem --no-emit-version ---card-edit --no-encrypt-to ---card-status --no-escape-from-lines ---cert-digest-algo --no-expensive-trust-checks ---cert-notation --no-expert ---cert-policy-url --no-force-mdc ---change-pin --no-force-v3-sigs ---charset --no-force-v4-certs ---check-sig --no-for-your-eyes-only ---check-sigs --no-greeting ---check-trustdb --no-groups ---cipher-algo --no-literal ---clearsign --no-mangle-dos-filenames ---command-fd --no-mdc-warning ---command-file --no-options ---comment --no-permission-warning ---completes-needed --no-pgp2 ---compress-algo --no-pgp6 ---compression-algo --no-pgp7 ---compress-keys --no-pgp8 ---compress-level --no-random-seed-file ---compress-sigs --no-require-backsigs ---ctapi-driver --no-require-cross-certification ---dearmor --no-require-secmem ---dearmour --no-rfc2440-text ---debug --no-secmem-warning ---debug-all --no-show-notation ---debug-ccid-driver --no-show-photos ---debug-level --no-show-policy-url ---decrypt --no-sig-cache ---decrypt-files --no-sig-create-check ---default-cert-check-level --no-sk-comments ---default-cert-expire --no-strict ---default-cert-level --notation-data ---default-comment --not-dash-escaped ---default-key --no-textmode ---default-keyserver-url --no-throw-keyid ---default-preference-list --no-throw-keyids ---default-recipient --no-tty ---default-recipient-self --no-use-agent ---default-sig-expire --no-use-embedded-filename ---delete-keys --no-utf8-strings ---delete-secret-and-public-keys --no-verbose ---delete-secret-keys --no-version ---desig-revoke --openpgp ---detach-sign --options ---digest-algo --output ---disable-ccid --override-session-key ---disable-cipher-algo --passphrase ---disable-dsa2 --passphrase-fd ---disable-mdc --passphrase-file ---disable-pubkey-algo --passphrase-repeat ---display --pcsc-driver ---display-charset --personal-cipher-preferences ---dry-run --personal-cipher-prefs ---dump-options --personal-compress-preferences ---edit-key --personal-compress-prefs ---emit-version --personal-digest-preferences ---enable-dsa2 --personal-digest-prefs ---enable-progress-filter --pgp2 ---enable-special-filenames --pgp6 ---enarmor --pgp7 ---enarmour --pgp8 ---encrypt --photo-viewer ---encrypt-files --pipemode ---encrypt-to --preserve-permissions ---escape-from-lines --primary-keyring ---exec-path --print-md ---exit-on-status-write-error --print-mds ---expert --quick-random ---export --quiet ---export-options --reader-port ---export-ownertrust --rebuild-keydb-caches ---export-secret-keys --recipient ---export-secret-subkeys --recv-keys ---fast-import --refresh-keys ---fast-list-mode --remote-user ---fetch-keys --require-backsigs ---fingerprint --require-cross-certification ---fixed-list-mode --require-secmem ---fix-trustdb --rfc1991 ---force-mdc --rfc2440 ---force-ownertrust --rfc2440-text ---force-v3-sigs --rfc4880 ---force-v4-certs --run-as-shm-coprocess ---for-your-eyes-only --s2k-cipher-algo ---gen-key --s2k-count ---gen-prime --s2k-digest-algo ---gen-random --s2k-mode ---gen-revoke --search-keys ---gnupg --secret-keyring ---gpg-agent-info --send-keys ---gpgconf-list --set-filename ---gpgconf-test --set-filesize ---group --set-notation ---help --set-policy-url ---hidden-encrypt-to --show-keyring ---hidden-recipient --show-notation ---homedir --show-photos ---honor-http-proxy --show-policy-url ---ignore-crc-error --show-session-key ---ignore-mdc-error --sig-keyserver-url ---ignore-time-conflict --sign ---ignore-valid-from --sign-key ---import --sig-notation ---import-options --sign-with ---import-ownertrust --sig-policy-url ---interactive --simple-sk-checksum ---keyid-format --sk-comments ---keyring --skip-verify ---keyserver --status-fd ---keyserver-options --status-file ---lc-ctype --store ---lc-messages --strict ---limit-card-insert-tries --symmetric ---list-config --temp-directory ---list-key --textmode ---list-keys --throw-keyid ---list-only --throw-keyids ---list-options --trustdb-name ---list-ownertrust --trusted-key ---list-packets --trust-model ---list-public-keys --try-all-secrets ---list-secret-keys --ttyname ---list-sig --ttytype ---list-sigs --ungroup ---list-trustdb --update-trustdb ---load-extension --use-agent ---local-user --use-embedded-filename ---lock-multiple --user ---lock-never --utf8-strings ---lock-once --verbose ---logger-fd --verify ---logger-file --verify-files ---lsign-key --verify-options ---mangle-dos-filenames --version ---marginals-needed --warranty ---max-cert-depth --with-colons ---max-output --with-fingerprint ---merge-only --with-key-data ---min-cert-level --yes -""").split() - - possible = frozenset(three_hundred_eighteen) + gnupg_options = _get_all_gnupg_options() + allowed = _get_options_group("allowed") ## these are the allowed options we will handle so far, all others should ## be dropped. this dance is so that when new options are added later, we ## merely add the to the _allowed list, and the `` _allowed.issubset`` ## assertion will check that GPG will recognise them - ## - ## xxx checkout the --store option for creating rfc1991 data packets - ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] - ## - allowed = frozenset(['--fixed-list-mode', ## key/packet listing - '--list-key', - '--list-keys', - '--list-options', - '--list-packets', - '--list-public-keys', - '--list-secret-keys', - '--print-md', - '--print-mds', - '--with-colons', - ## deletion - '--delete-keys', - '--delete-secret-keys', - ## en-/de-cryption - '--always-trust', - '--decrypt', - '--decrypt-files', - '--encrypt', - '--encrypt-files', - '--recipient', - '--no-default-recipient', - '--symmetric', - '--use-agent', - '--no-use-agent', - ## signing/certification - '--armor', - '--armour', - '--clearsign', - '--detach-sign', - '--list-sigs', - '--sign', - '--verify', - ## i/o and files - '--batch', - '--debug-all', - '--debug-level', - '--gen-key', - #'--multifile', - '--no-emit-version', - '--no-tty', - '--output', - '--passphrase-fd', - '--status-fd', - '--version', - ## keyring, homedir, & options - '--homedir', - '--keyring', - '--primary-keyring', - '--secret-keyring', - '--no-default-keyring', - '--default-key', - '--no-options', - '--keyserver', - '--recv-keys', - '--send-keys', - ## preferences - '--digest-algo', - '--cipher-algo', - '--compress-algo', - '--compression-algo', - '--cert-digest-algo', - '--list-config', - '--personal-digest-prefs', - '--personal-digest-preferences', - '--personal-cipher-prefs', - '--personal-cipher-preferences', - '--personal-compress-prefs', - '--personal-compress-preferences', - ## export/import - '--import', - '--export', - '--export-secret-keys', - '--export-secret-subkeys', - '--fingerprint', - ]) - try: - ## check that allowed is a subset of possible - assert allowed.issubset(possible) + ## check that allowed is a subset of all gnupg_options + assert allowed.issubset(gnupg_options) except AssertionError: raise UsageError("'allowed' isn't a subset of known options, diff: %s" - % allowed.difference(possible)) + % allowed.difference(gnupg_options)) ## if we got a list of args, join them ## @@ -504,7 +262,10 @@ def _sanitise(*args): :rtype: str :returns: A string of the items in ``checked`` delimited by spaces. """ - safe_option = str() + checked = str() + none_options = _get_options_group("none_options") + hex_options = _get_options_group("hex_options") + hex_or_none_options = _get_options_group("hex_or_none_options") if not _util._py3k: if not isinstance(arg, list) and isinstance(arg, unicode): @@ -667,6 +428,313 @@ def _sanitise_list(arg_list): if safe_arg != "": yield safe_arg +def _get_options_group(group=None): + """Get a specific group of options which are allowed.""" + + #: These expect a hexidecimal keyid as their argument, and can be parsed + #: with :func:`_is_hex`. + hex_options = frozenset(['--check-sigs', + '--default-key', + '--default-recipient', + '--delete-keys', + '--delete-secret-keys', + '--delete-secret-and-public-keys', + '--desig-revoke', + '--export', + '--export-secret-keys', + '--export-secret-subkeys', + '--fingerprint', + '--gen-revoke', + '--list-key', + '--list-keys', + '--list-public-keys', + '--list-secret-keys', + '--list-sigs', + '--recipient', + '--recv-keys', + '--send-keys', + ]) + #: These options expect value which are left unchecked, though still run + #: through :func:`_fix_unsafe`. + unchecked_options = frozenset(['--list-options', + '--passphrase-fd', + '--status-fd', + '--verify-options', + ]) + #: These have their own parsers and don't really fit into a group + other_options = frozenset(['--debug-level', + '--keyserver', + + ]) + #: These should have a directory for an argument + dir_options = frozenset(['--homedir', + ]) + #: These expect a keyring or keyfile as their argument + keyring_options = frozenset(['--keyring', + '--primary-keyring', + '--secret-keyring', + '--trustdb-name', + ]) + #: These expect a filename (or the contents of a file as a string) or None + #: (meaning that they read from stdin) + file_or_none_options = frozenset(['--decrypt', + '--decrypt-files', + '--encrypt', + '--encrypt-files', + '--import', + '--verify', + '--verify-files', + ]) + #: These options expect a string. see :func:`_check_preferences`. + pref_options = frozenset(['--digest-algo', + '--cipher-algo', + '--compress-algo', + '--compression-algo', + '--cert-digest-algo', + '--personal-digest-prefs', + '--personal-digest-preferences', + '--personal-cipher-prefs', + '--personal-cipher-preferences', + '--personal-compress-prefs', + '--personal-compress-preferences', + '--print-md', + ]) + #: These options expect no arguments + none_options = frozenset(['--always-trust', + '--armor', + '--armour', + '--batch', + '--check-sigs', + '--check-trustdb', + '--clearsign', + '--debug-all', + '--default-recipient-self', + '--detach-sign', + '--export', + '--export-secret-keys', + '--export-secret-subkeys', + '--fingerprint', + '--fixed-list-mode', + '--gen-key', + '--list-config', + '--list-key', + '--list-keys', + '--list-packets', + '--list-public-keys', + '--list-secret-keys', + '--list-sigs', + '--no-default-keyring', + '--no-default-recipient', + '--no-emit-version', + '--no-options', + '--no-tty', + '--no-use-agent', + '--no-verbose', + '--print-mds', + '--quiet', + '--sign', + '--symmetric', + '--use-agent', + '--verbose', + '--version', + '--with-colons', + '--yes', + ]) + #: These options expect either None or a hex string + hex_or_none_options = hex_options.intersection(none_options) + allowed = hex_options.union(unchecked_options, other_options, dir_options, + keyring_options, file_or_none_options, + pref_options, none_options) + + if group and group in locals().keys(): + return locals()[group] + +def _get_all_gnupg_options(): + """Get all GnuPG options and flags. + + This is hardcoded within a local scope to reduce the chance of a tampered + GnuPG binary reporting falsified option sets, i.e. because certain options + (namedly the '--no-options' option, which prevents the usage of gpg.conf + files) are necessary and statically specified in + :meth:`gnupg.GPG._makeargs`, if the inputs into Python are already + controlled, and we were to summon the GnuPG binary to ask it for its + options, it would be possible to receive a falsified options set missing + the '--no-options' option in response. This seems unlikely, and the method + is stupid and ugly, but at least we'll never have to debug whether or not + an option *actually* disappeared in a different GnuPG version, or some + funny business is happening. + + These are the options as of GnuPG 1.4.12; the current stable branch of the + 2.1.x tree contains a few more -- if you need them you'll have to add them + in here. + + :ivar frozenset gnupg_options: All known GPG options and flags. + :rtype: frozenset + :returns: ``gnupg_options`` + """ + three_hundred_eighteen = (""" +--allow-freeform-uid --multifile +--allow-multiple-messages --no +--allow-multisig-verification --no-allow-freeform-uid +--allow-non-selfsigned-uid --no-allow-multiple-messages +--allow-secret-key-import --no-allow-non-selfsigned-uid +--always-trust --no-armor +--armor --no-armour +--armour --no-ask-cert-expire +--ask-cert-expire --no-ask-cert-level +--ask-cert-level --no-ask-sig-expire +--ask-sig-expire --no-auto-check-trustdb +--attribute-fd --no-auto-key-locate +--attribute-file --no-auto-key-retrieve +--auto-check-trustdb --no-batch +--auto-key-locate --no-comments +--auto-key-retrieve --no-default-keyring +--batch --no-default-recipient +--bzip2-compress-level --no-disable-mdc +--bzip2-decompress-lowmem --no-emit-version +--card-edit --no-encrypt-to +--card-status --no-escape-from-lines +--cert-digest-algo --no-expensive-trust-checks +--cert-notation --no-expert +--cert-policy-url --no-force-mdc +--change-pin --no-force-v3-sigs +--charset --no-force-v4-certs +--check-sig --no-for-your-eyes-only +--check-sigs --no-greeting +--check-trustdb --no-groups +--cipher-algo --no-literal +--clearsign --no-mangle-dos-filenames +--command-fd --no-mdc-warning +--command-file --no-options +--comment --no-permission-warning +--completes-needed --no-pgp2 +--compress-algo --no-pgp6 +--compression-algo --no-pgp7 +--compress-keys --no-pgp8 +--compress-level --no-random-seed-file +--compress-sigs --no-require-backsigs +--ctapi-driver --no-require-cross-certification +--dearmor --no-require-secmem +--dearmour --no-rfc2440-text +--debug --no-secmem-warning +--debug-all --no-show-notation +--debug-ccid-driver --no-show-photos +--debug-level --no-show-policy-url +--decrypt --no-sig-cache +--decrypt-files --no-sig-create-check +--default-cert-check-level --no-sk-comments +--default-cert-expire --no-strict +--default-cert-level --notation-data +--default-comment --not-dash-escaped +--default-key --no-textmode +--default-keyserver-url --no-throw-keyid +--default-preference-list --no-throw-keyids +--default-recipient --no-tty +--default-recipient-self --no-use-agent +--default-sig-expire --no-use-embedded-filename +--delete-keys --no-utf8-strings +--delete-secret-and-public-keys --no-verbose +--delete-secret-keys --no-version +--desig-revoke --openpgp +--detach-sign --options +--digest-algo --output +--disable-ccid --override-session-key +--disable-cipher-algo --passphrase +--disable-dsa2 --passphrase-fd +--disable-mdc --passphrase-file +--disable-pubkey-algo --passphrase-repeat +--display --pcsc-driver +--display-charset --personal-cipher-preferences +--dry-run --personal-cipher-prefs +--dump-options --personal-compress-preferences +--edit-key --personal-compress-prefs +--emit-version --personal-digest-preferences +--enable-dsa2 --personal-digest-prefs +--enable-progress-filter --pgp2 +--enable-special-filenames --pgp6 +--enarmor --pgp7 +--enarmour --pgp8 +--encrypt --photo-viewer +--encrypt-files --pipemode +--encrypt-to --preserve-permissions +--escape-from-lines --primary-keyring +--exec-path --print-md +--exit-on-status-write-error --print-mds +--expert --quick-random +--export --quiet +--export-options --reader-port +--export-ownertrust --rebuild-keydb-caches +--export-secret-keys --recipient +--export-secret-subkeys --recv-keys +--fast-import --refresh-keys +--fast-list-mode --remote-user +--fetch-keys --require-backsigs +--fingerprint --require-cross-certification +--fixed-list-mode --require-secmem +--fix-trustdb --rfc1991 +--force-mdc --rfc2440 +--force-ownertrust --rfc2440-text +--force-v3-sigs --rfc4880 +--force-v4-certs --run-as-shm-coprocess +--for-your-eyes-only --s2k-cipher-algo +--gen-key --s2k-count +--gen-prime --s2k-digest-algo +--gen-random --s2k-mode +--gen-revoke --search-keys +--gnupg --secret-keyring +--gpg-agent-info --send-keys +--gpgconf-list --set-filename +--gpgconf-test --set-filesize +--group --set-notation +--help --set-policy-url +--hidden-encrypt-to --show-keyring +--hidden-recipient --show-notation +--homedir --show-photos +--honor-http-proxy --show-policy-url +--ignore-crc-error --show-session-key +--ignore-mdc-error --sig-keyserver-url +--ignore-time-conflict --sign +--ignore-valid-from --sign-key +--import --sig-notation +--import-options --sign-with +--import-ownertrust --sig-policy-url +--interactive --simple-sk-checksum +--keyid-format --sk-comments +--keyring --skip-verify +--keyserver --status-fd +--keyserver-options --status-file +--lc-ctype --store +--lc-messages --strict +--limit-card-insert-tries --symmetric +--list-config --temp-directory +--list-key --textmode +--list-keys --throw-keyid +--list-only --throw-keyids +--list-options --trustdb-name +--list-ownertrust --trusted-key +--list-packets --trust-model +--list-public-keys --try-all-secrets +--list-secret-keys --ttyname +--list-sig --ttytype +--list-sigs --ungroup +--list-trustdb --update-trustdb +--load-extension --use-agent +--local-user --use-embedded-filename +--lock-multiple --user +--lock-never --utf8-strings +--lock-once --verbose +--logger-fd --verify +--logger-file --verify-files +--lsign-key --verify-options +--mangle-dos-filenames --version +--marginals-needed --warranty +--max-cert-depth --with-colons +--max-output --with-fingerprint +--merge-only --with-key-data +--min-cert-level --yes +""").split() + gnupg_options = frozenset(three_hundred_eighteen) + return gnupg_options def nodata(status_code): """Translate NODATA status codes from GnuPG to messages.""" From 4fcfef1bdef44db60b8ab1d04900ccadeb9e0b2b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 02:29:36 +0000 Subject: [PATCH 120/145] Add _parsers._is_string() function, and change _sanitise() to use it. --- gnupg/_parsers.py | 60 ++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 6ea4963..da9a6d0 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -215,6 +215,19 @@ def _is_hex(string): return True return False +def _is_string(thing): + """Python character arrays are a mess. + + If Python2, check if ``thing`` is a ``unicode()`` or ``str()``. + If Python3, check if ``thing`` is a ``str()``. + + :param thing: The thing to check. + :returns: ``True`` if ``thing`` is a "string" according to whichever + version of Python we're running in. + """ + if _util._py3k: return isinstance(thing, str) + else: return isinstance(thing, basestring) + def _sanitise(*args): """Take an arg or the key portion of a kwarg and check that it is in the set of allowed GPG options and flags, and that it has the correct @@ -277,40 +290,39 @@ def _sanitise(*args): except (AssertionError, ProtectedOption) as error: log.warn("_check_option(): %s" % error.message) else: - safe_option += (flag + ' ') - if (not _util._py3k and isinstance(value, basestring)) \ - or (_util._py3k and isinstance(value, str)): + checked += (flag + ' ') + + if _is_string(value): values = value.split(' ') for v in values: - try: - assert v is not None - assert not v.isspace() - except: - log.debug("Dropping %s %s" % (flag, v)) - continue - ## these can be handled separately, without _fix_unsafe(), ## because they are only allowed if the pass the regex - if flag in ['--default-key', '--recipient', '--export', - '--export-secret-keys', '--delete-keys', - '--list-sigs', '--export-secret-subkeys', - '--recv-keys']: - if _is_hex(v): safe_option += (v + " ") - else: log.debug("'%s %s' not hex." % (flag, v)) + if (flag in none_options) and (v is None): + continue + + if flag in hex_options: + if _is_hex(v): checked += (v + " ") + else: + log.debug("'%s %s' not hex." % (flag, v)) + if (flag in hex_or_none_options) and (v is None): + log.debug("Allowing '%s' for all keys" % flag) continue elif flag in ['--keyserver']: host = _check_keyserver(v) if host: log.debug("Setting keyserver: %s" % host) - safe_option += (v + " ") + checked += (v + " ") else: log.debug("Dropping keyserver: %s" % v) continue + ## the rest are strings, filenames, etc, and should be + ## shell escaped: val = _fix_unsafe(v) - try: - assert v is not None + assert not val is None + assert not val.isspace() + assert not v is None assert not v.isspace() except: log.debug("Dropping %s %s" % (flag, v)) @@ -318,27 +330,27 @@ def _sanitise(*args): if flag in ['--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', '--import', '--verify']: - if _util._is_file(val): safe_option += (val + " ") + if _util._is_file(val): checked += (val + " ") else: log.debug("%s not file: %s" % (flag, val)) elif flag in ['--cipher-algo', '--personal-cipher-prefs', '--personal-cipher-preferences']: legit_algos = _check_preferences(val, 'cipher') - if legit_algos: safe_option += (legit_algos + " ") + if legit_algos: checked += (legit_algos + " ") else: log.debug("'%s' is not cipher" % val) elif flag in ['--compress-algo', '--compression-algo', '--personal-compress-prefs', '--personal-compress-preferences']: legit_algos = _check_preferences(val, 'compress') - if legit_algos: safe_option += (legit_algos + " ") + if legit_algos: checked += (legit_algos + " ") else: log.debug("'%s' not compress algo" % val) else: - safe_option += (val + " ") + checked += (val + " ") log.debug("_check_option(): No checks for %s" % val) - return safe_option + return checked is_flag = lambda x: x.startswith('--') From f996bf9e314daa1bf0d0ff1b96b450213189793d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 02:30:20 +0000 Subject: [PATCH 121/145] Update _parsers._sanitise_list() documentation. --- gnupg/_parsers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index da9a6d0..1c93640 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -430,9 +430,9 @@ def _sanitise_list(arg_list): :param list arg_list: A list of options and flags for GnuPG. :rtype: generator - :return: A generator whose next() method returns each of the items in - ``arg_list`` after calling ``_sanitise()`` with that item as a - parameter. + :returns: A generator whose next() method returns each of the items in + ``arg_list`` after calling ``_sanitise()`` with that item as a + parameter. """ if isinstance(arg_list, list): for arg in arg_list: From d129ed5b1725fb46972a26c1f76d1f75b068b0cb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 02:30:57 +0000 Subject: [PATCH 122/145] Add _parsers.GenKey.secring and _parsers.GenKey.keyring attributes and docs. --- gnupg/_parsers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 1c93640..67dc592 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -790,6 +790,12 @@ class GenKey(object): self.status = None self.subkey_created = False self.primary_created = False + #: This will store the filename of the key's public keyring if + #: :meth:`GPG.gen_key_input` was called with ``separate_keyring=True`` + self.keyring = None + #: This will store the filename of the key's secret keyring if + #: :meth:`GPG.gen_key_input` was called with ``separate_keyring=True`` + self.secring = None def __nonzero__(self): if self.fingerprint: return True From 9d5cdb885479a14f74704ff2ea608a72a29885e6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 02:31:37 +0000 Subject: [PATCH 123/145] Fix upstream issue #62, handle expired keys. * see https://code.google.com/p/python-gnupg/issues/detail?id=62 --- gnupg/_parsers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 67dc592..0d17de2 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -900,11 +900,15 @@ class Sign(object): """ if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", - "INV_SGNR"): + "INV_SGNR", "SIGEXPIRED"): pass elif key == "SIG_CREATED": (self.sig_type, self.sig_algo, self.sig_hash_algo, self.what, self.timestamp, self.fingerprint) = value.split() + elif key == "KEYEXPIRED": + self.status = "skipped signing key, key expired" + if (value is not None) and (len(value) > 0): + self.status += " on {}".format(str(value)) elif key == "NODATA": self.status = nodata(value) else: From 328e8fcbe08b32bcf0094e2819be40730f11aee6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 06:28:58 +0000 Subject: [PATCH 124/145] Add a 'generated-keys' subdirectory to gnupg.GPG.homedir. * Add GPG._generated_keys as an _utils.InheritableProperty class for storing a configurable (even by subclasses of GPGBase, without property overrides) subdirectory of whichever directory gnupg.GPG.homedir is set to. This subdirectory can be used via the 'separate_keyring=True' option to gnupg.GPG.gen_key_input(), which will switch temporarily to new pubring.gpg and secring.gpg keyrings during key creation, and upon finishing creation of the new key, will ask for the new key's fingerprint, and move the keyrings into this GPG._generated_keys directory, renamed in the format "//.[pub|sec]ring.gpg". --- gnupg/_meta.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index d58d174..8729a2d 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -121,6 +121,7 @@ class GPGBase(object): sys.getfilesystemencoding().lower()) self._keyserver = 'hkp://subkeys.pgp.net' + self.__generated_keys = os.path.join(self.homedir, 'generated-keys') try: assert self.binary, "Could not find binary %s" % binary @@ -342,6 +343,58 @@ class GPGBase(object): homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter) + def _generated_keys_getter(self): + """Get the ``homedir`` subdirectory for storing generated keys. + + :rtype: str + :returns: The absolute path to the current GnuPG homedir. + """ + return self.__generated_keys + + def _generated_keys_setter(self, directory): + """Set the directory for storing generated keys. + + If unspecified, use $GNUPGHOME/generated-keys. If specified, ensure + that the ``directory`` does not contain various shell escape + characters. If ``directory`` is not found, it will be automatically + created. Lastly, the ``direcory`` will be checked that the EUID has + read and write permissions for it. + + :param str directory: A relative or absolute path to the directory to + use for storing/accessing GnuPG's files, including keyrings and + the trustdb. + :raises: :exc:`RuntimeError` if unable to find a suitable directory to + use. + """ + if not directory: + directory = os.path.join(self.homedir, 'generated-keys') + log.debug("GPGBase._generated_keys_setter(): Using '%s'" + % directory) + + hd = _parsers._fix_unsafe(directory) + log.debug("GPGBase._generated_keys_setter(): got directory '%s'" % hd) + + if hd: + log.debug("GPGBase._generated_keys_setter(): Check exists '%s'" + % hd) + _util._create_if_necessary(hd) + + try: + log.debug("GPGBase._generated_keys_setter(): check permissions") + assert _util._has_readwrite(hd), \ + "Keys dir '%s' needs read/write permissions" % hd + except AssertionError as ae: + msg = ("Unable to set '%s' as generated keys dir" % directory) + log.debug("GPGBase._generated_keys_setter(): %s" % msg) + log.debug(ae.message) + raise RuntimeError(ae.message) + else: + log.info("Setting homedir to '%s'" % hd) + self.__generated_keys = hd + + _generated_keys = _util.InheritableProperty(_generated_keys_getter, + _generated_keys_setter) + def _make_args(self, args, passphrase=False): """Make a list of command line elements for GPG. The value of ``args`` will be appended only if it passes the checks in From 603379eb37c3b743ba49aab2d6563e1caa9adbe0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 06:40:09 +0000 Subject: [PATCH 125/145] Change _copy_data() error handling to explain errors not due to broken pipes. --- gnupg/_util.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index 3cb6342..3ce7c11 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -145,9 +145,12 @@ def _copy_data(instream, outstream): except IOError: log.exception("Error sending data: Broken pipe") break - except IOError: + except IOError as ioe: # Can get 'broken pipe' errors even when all data was sent - log.exception('Error sending data: Broken pipe') + if 'Broken pipe' in ioe.message: + log.error('Error sending data: Broken pipe') + else: + log.exception(ioe) break try: outstream.close() From 9008faa0db6f8f9eba23dc3d9c1eca5ad0bda85f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 06:42:42 +0000 Subject: [PATCH 126/145] =?UTF-8?q?Fix=20a=20misnamed=20variable=20in=20gn?= =?UTF-8?q?upg.py;=20self.=5Fkey=5Fdir=E2=86=92self.=5Fkeys=5Fdir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gnupg/gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index a7cb39b..2e76e52 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -497,9 +497,9 @@ class GPG(GPGBase): fpr = str(key.fingerprint) if len(fpr) == 20: if self.temp_keyring or self.temp_secring: - if not os.path.exists(self._key_dir): - os.makedirs(self._key_dir) - prefix = os.path.join(self._key_dir, fpr) + if not os.path.exists(self._keys_dir): + os.makedirs(self._keys_dir) + prefix = os.path.join(self._keys_dir, fpr) if self.temp_keyring: if os.path.isfile(self.temp_keyring): From 8232f14f2a3d8f0f94d4b45831f6026883d93185 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 06:44:52 +0000 Subject: [PATCH 127/145] Remove commented out code from GPG.gen_key() for import after generation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Generated keys in separate keyring would not be available after switching back to the normal keyrings (not the ones set up for key generation), so at first in the unittests I imported them back into the main keyrings… this, however, is would be stupid, as in nullifies half the reasons for using separate keyrings in the first place, thus the code was commented out. Now that the temporary keyrings are placed in the gnupg.GPG._generated_keys directory, it would be nice to eventually either extend GPG.import_keys() or add a new helper function for doing: $ gpg --no-default-keyring --keyring pubring.gpg \ --keyring ./generated-keys/.gpg --- gnupg/gnupg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 2e76e52..9104526 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -506,14 +506,12 @@ class GPG(GPGBase): try: os.rename(self.temp_keyring, prefix+".pubring") except OSError as ose: log.error(ose.message) else: self.temp_keyring = None - #finally: self.import_keys(fpr) if self.temp_secring: if os.path.isfile(self.temp_secring): try: os.rename(self.temp_secring, prefix+".secring") except OSError as ose: log.error(ose.message) else: self.temp_secring = None - #finally: self.import_keys(fpr) log.info("Key created. Fingerprint: %s" % fpr) return key From f11ea6901cdd0a01ac713c23f8635c9e86a3fa7d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 07:09:35 +0000 Subject: [PATCH 128/145] Set gpg.temp_[key|sec]ring to None after setting key.[key|sec]ring. * Change the returned _parsers.GenKey from gnupg.GPG.gen_key() to store the location of the renamed temporary keyrings as attributes. This way, one can do something like: >>> key = gpg.gen_key(key_input) >>> key.keyring './generated-keys/328A5C6C1B2F0891125ECBE4624276B5A2296478.pubring.gpg' --- gnupg/gnupg.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 9104526..102752f 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -505,15 +505,18 @@ class GPG(GPGBase): if os.path.isfile(self.temp_keyring): try: os.rename(self.temp_keyring, prefix+".pubring") except OSError as ose: log.error(ose.message) - else: self.temp_keyring = None if self.temp_secring: if os.path.isfile(self.temp_secring): try: os.rename(self.temp_secring, prefix+".secring") except OSError as ose: log.error(ose.message) - else: self.temp_secring = None log.info("Key created. Fingerprint: %s" % fpr) + key.keyring = self.temp_keyring + key.secring = self.temp_secring + self.temp_keyring = None + self.temp_secring = None + return key def gen_key_input(self, separate_keyring=False, save_batchfile=False, From ad5c282e9aafb0f9711b54de70b78d2a92be59bd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 07:17:12 +0000 Subject: [PATCH 129/145] Change setup.py to rename the package from 'python-gnupg' to 'gnupg'. * To import, the same 'import gnupg; gpg=gnupg.GPG' is still used, so this changes nothing for applications which have started using this fork. * This name appears to be untaken on PyPI, the changes I've made thus far are significant, and, I believe, are not entirely backwards compatible with upstream, do to various keyword argument and method renamings and such. * Upstream is unresponsive, their development is private, and the only way to get their code is to download an entire tarball and do a diff, since no public CVS is available. Also, looking at their recent releases in the past five months, they appear to be fixing bugs I've already fixed, albeit in different ways -- and not all of the bugs I've fixed, as the shell escape trick still works in upstream's 0.3.4 version -- which leads me to believe that they are generally oblivious to this fork, though I have tried to contact them several times. * At first, I merely bumped the major version number up to indicate the incompatibility, but in order to upload to PyPI it would need a different name, and so I might as well pick a new one and just go with it, in order to avoid the general confusion which might result from keeping the name 'python-gnupg'. --- setup.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index d1a76f9..c5fdff0 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ import versioneer versioneer.versionfile_source = 'src/_version.py' versioneer.versionfile_build = 'gnupg/_version.py' versioneer.tag_prefix = '' -versioneer.parentdir_prefix = 'python-gnupg-' +versioneer.parentdir_prefix = 'gnupg-' __author__ = "Isis Agora Lovecruft" __contact__ = 'isis@leap.se' @@ -79,11 +79,15 @@ def run_distribute_setup_script(): os.system(script) setuptools.setup( - name = "python-gnupg", + name = "gnupg", description="A Python wrapper for GnuPG", - long_description = "This module allows easy access to GnuPG's key \ -management, encryption and signature functionality from Python programs. \ -It is intended for use with Python 2.6 or greater.", + long_description = """\ +This module allows easy access to GnuPG's key management, encryption and \ +signature functionality from Python programs, by interacting with GnuPG \ +through file descriptors. Input arguments are strictly checked and sanitised, \ +and therefore this module should be safe to use in networked applications \ +requiring direct user input. It is intended for use with Python 2.6 or \ +greater.""", license="AGPLv3", version=versioneer.get_version(), From 1780c7489abeeb9f5929b90ca1b884d66379a281 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 07:27:07 +0000 Subject: [PATCH 130/145] Change the _generated_keys directory for unittests to be under tests/files/. --- tests/test_gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 0cb983c..d7e351e 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -191,6 +191,7 @@ class GPGTestCase(unittest.TestCase): self.keyring = self.gpg.keyring self.secring = self.gpg.secring self.insecure_prng = False + self.gpg._keys_dir = os.path.join(_files, 'generated-keys') def tearDown(self): """This is called once per self.test_* method after the test run.""" From 220e68bdfc011b9eac6b0b36bad39b27fc01a753 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 07:27:57 +0000 Subject: [PATCH 131/145] Change unittest for separate keyrings to add extra --keyring option for now. * At some point, as mentioned in an earlier commit, we should actually make some sort of helper function for adding extra keyring, so that their keys can be used/imported/exported/whatever the user wants to do with them, without changing the normal main keyrings. --- tests/test_gnupg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index d7e351e..a3b65a2 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -788,10 +788,14 @@ authentication.""" 'subkey_usage': 'encrypt,sign', 'passphrase': 'overalls' } + extra_keyrings = [] + ian_input = self.gpg.gen_key_input(separate_keyring=True, **ian) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) ian_key = self.gpg.gen_key(ian_input) ian_fpr = str(ian_key.fingerprint) + extra_keyrings.append('--keyring %s' % ian_key.pubring) + self.gpg.options = extra_keyrings kat_input = self.gpg.gen_key_input(separate_keyring=True, **kat) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) From d7a0ffb3950c54e4cd952320c8ce28214ae27b1d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 13:13:27 +0000 Subject: [PATCH 132/145] =?UTF-8?q?Move=20tests/*=20=E2=86=92=20gnupg/test?= =?UTF-8?q?/*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {tests => gnupg/test}/__init__.py | 0 {tests => gnupg/test}/doctests/README | 0 {tests => gnupg/test}/files/cypherpunk_manifesto | 0 {tests => gnupg/test}/files/kat.pub | 0 {tests => gnupg/test}/files/kat.sec | 0 {tests => gnupg/test}/files/test_key_1.pub | 0 {tests => gnupg/test}/files/test_key_1.sec | 0 {tests => gnupg/test}/files/test_key_2.pub | 0 {tests => gnupg/test}/files/test_key_2.sec | 0 {tests => gnupg/test}/test_gnupg.py | 6 ++---- 10 files changed, 2 insertions(+), 4 deletions(-) rename {tests => gnupg/test}/__init__.py (100%) rename {tests => gnupg/test}/doctests/README (100%) rename {tests => gnupg/test}/files/cypherpunk_manifesto (100%) rename {tests => gnupg/test}/files/kat.pub (100%) rename {tests => gnupg/test}/files/kat.sec (100%) rename {tests => gnupg/test}/files/test_key_1.pub (100%) rename {tests => gnupg/test}/files/test_key_1.sec (100%) rename {tests => gnupg/test}/files/test_key_2.pub (100%) rename {tests => gnupg/test}/files/test_key_2.sec (100%) rename {tests => gnupg/test}/test_gnupg.py (99%) diff --git a/tests/__init__.py b/gnupg/test/__init__.py similarity index 100% rename from tests/__init__.py rename to gnupg/test/__init__.py diff --git a/tests/doctests/README b/gnupg/test/doctests/README similarity index 100% rename from tests/doctests/README rename to gnupg/test/doctests/README diff --git a/tests/files/cypherpunk_manifesto b/gnupg/test/files/cypherpunk_manifesto similarity index 100% rename from tests/files/cypherpunk_manifesto rename to gnupg/test/files/cypherpunk_manifesto diff --git a/tests/files/kat.pub b/gnupg/test/files/kat.pub similarity index 100% rename from tests/files/kat.pub rename to gnupg/test/files/kat.pub diff --git a/tests/files/kat.sec b/gnupg/test/files/kat.sec similarity index 100% rename from tests/files/kat.sec rename to gnupg/test/files/kat.sec diff --git a/tests/files/test_key_1.pub b/gnupg/test/files/test_key_1.pub similarity index 100% rename from tests/files/test_key_1.pub rename to gnupg/test/files/test_key_1.pub diff --git a/tests/files/test_key_1.sec b/gnupg/test/files/test_key_1.sec similarity index 100% rename from tests/files/test_key_1.sec rename to gnupg/test/files/test_key_1.sec diff --git a/tests/files/test_key_2.pub b/gnupg/test/files/test_key_2.pub similarity index 100% rename from tests/files/test_key_2.pub rename to gnupg/test/files/test_key_2.pub diff --git a/tests/files/test_key_2.sec b/gnupg/test/files/test_key_2.sec similarity index 100% rename from tests/files/test_key_2.sec rename to gnupg/test/files/test_key_2.sec diff --git a/tests/test_gnupg.py b/gnupg/test/test_gnupg.py similarity index 99% rename from tests/test_gnupg.py rename to gnupg/test/test_gnupg.py index a3b65a2..cefe1a7 100644 --- a/tests/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -788,19 +788,17 @@ authentication.""" 'subkey_usage': 'encrypt,sign', 'passphrase': 'overalls' } - extra_keyrings = [] - ian_input = self.gpg.gen_key_input(separate_keyring=True, **ian) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) ian_key = self.gpg.gen_key(ian_input) ian_fpr = str(ian_key.fingerprint) - extra_keyrings.append('--keyring %s' % ian_key.pubring) - self.gpg.options = extra_keyrings + self.gpg.options = ['--keyring {}'.format(ian_key.pubring)] kat_input = self.gpg.gen_key_input(separate_keyring=True, **kat) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) kat_key = self.gpg.gen_key(kat_input) kat_fpr = str(kat_key.fingerprint) + self.gpg.options.append('--keyring {}'.format(kat_key.pubring)) self.gpg.import_keys(kat_key.data) message = """ From 8d3923488931a89d50fb5967d8078a2a6b121a3d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 13:13:58 +0000 Subject: [PATCH 133/145] Add test logs and files to gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ece3706..3378b9f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,9 @@ tags *random_seed* *.log tests/doctests/* +gnupg/test/doctests/* +gnupg/test/logs/* +gnupg/test/tmp/* # Ignore distutils record of installed files: installed-files.txt From 67e823dbf12795e1782e172cac8f6fe7c6361c64 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 28 Jun 2013 13:14:43 +0000 Subject: [PATCH 134/145] Add repoze.sphinx to docs dependency 'extras_require' setting in setup.py. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c5fdff0..30b09fa 100644 --- a/setup.py +++ b/setup.py @@ -104,7 +104,7 @@ greater.""", install_requires=reqs, dependency_links=deps, - extras_require={'docs': ["Sphinx>=1.1"]}, + extras_require={'docs': ["Sphinx>=1.1", "repoze.sphinx"]}, platforms="Linux, BSD, OSX, Windows", download_url="https://github.com/isislovecruft/python-gnupg/archive/master.zip", From 8e2773207b870e22ded388e42f7371dd9efa1407 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:36:48 +0000 Subject: [PATCH 135/145] Ignore setup.cfg maintainer upload configuration file. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3378b9f..7c66419 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ gnupg/test/tmp/* # Ignore distutils record of installed files: installed-files.txt +# Ignore maintainer setup.cfg file: +setup.cfg + # Ignore PyCharm files: .idea/* From 12b1de719afa23ad8c58ceb90d5a33a78cf5fef1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:42:17 +0000 Subject: [PATCH 136/145] Update paths in unittests to disregard the installed directory. --- gnupg/test/test_gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index cefe1a7..ae9f027 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -71,9 +71,9 @@ if __name__ == "__main__" and __package__ is None: log = _util.log log.setLevel(9) -print("Current directory: %s" % _util._here) -print("Current os directory: %s" % os.getcwd()) -_tests = os.path.join(os.path.join(os.getcwd(), 'gnupg'), 'test') +print("Current source directory: %s" % _util._here) +print("Current os.cwd directory: %s" % os.getcwd()) +_tests = os.path.join(os.path.join(_util._here, 'gnupg'), 'test') _files = os.path.join(_tests, 'files') _tempd = os.path.join(_tests, 'tmp') From 6cf7640c5cbef6ba30d63f8e99dcc0e7f6b1aa76 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:43:12 +0000 Subject: [PATCH 137/145] Fix whitespace formatting for a line in gnupg/tests/test_gnupg.py. --- gnupg/test/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index ae9f027..c2b02fb 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -80,7 +80,7 @@ _tempd = os.path.join(_tests, 'tmp') tempfile.tempdir = _tempd if not os.path.isdir(tempfile.gettempdir()): log.debug("Creating temporary testing directory: %s" - % tempfile.gettempdir()) + % tempfile.gettempdir()) os.makedirs(tempfile.gettempdir()) @wraps(tempfile.TemporaryFile) From 3b7457ae745c4a75500482948ea5a73a65ea0067 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:43:54 +0000 Subject: [PATCH 138/145] Remove a print statement from a unittest. --- gnupg/test/test_gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index c2b02fb..8a73086 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -218,7 +218,6 @@ class GPGTestCase(unittest.TestCase): """Test that we can't escape into the Python interpreter.""" shell_input = "; import antigravity ;" fixed = _parsers._fix_unsafe(shell_input) - print(fixed) def test_parsers_is_hex_valid(self): """Test that valid hexidecimal passes the parsers._is_hex() check""" From 61b27bcd3bf0fc0da3f83686750e21e87bdf8fc3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:54:05 +0000 Subject: [PATCH 139/145] Ignore additions to gnupg/tests/files/ directory. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7c66419..b6f0553 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ tests/doctests/* gnupg/test/doctests/* gnupg/test/logs/* gnupg/test/tmp/* +gnupg/test/files/* # Ignore distutils record of installed files: installed-files.txt From e18e10edd106da20553252417fc31ce90a9fa1d9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:55:26 +0000 Subject: [PATCH 140/145] Add a debug statement to a unittest when we've downloaded a key. --- gnupg/test/test_gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 8a73086..583c7f6 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -528,6 +528,7 @@ class GPGTestCase(unittest.TestCase): self.assertNotEquals(key, "") self.assertGreater(len(str(key)), 0) keyfile = os.path.join(self.gpg._keys_dir, 'test_key_3.pub') + log.debug("Storing downloaded key as %s" % keyfile) with open(keyfile, 'w') as fh: fh.write(key) self.assertTrue(os.path.isfile(keyfile)) From e8062c95aa8c3a51804830fdf0b0dd45f4793fa0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 00:55:53 +0000 Subject: [PATCH 141/145] =?UTF-8?q?Fix=20variable=20names=20in=20unittest:?= =?UTF-8?q?=20pubring=E2=86=92keyring.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gnupg/test/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 583c7f6..1b3c0f2 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -792,13 +792,13 @@ authentication.""" log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) ian_key = self.gpg.gen_key(ian_input) ian_fpr = str(ian_key.fingerprint) - self.gpg.options = ['--keyring {}'.format(ian_key.pubring)] + self.gpg.options = ['--keyring {}'.format(ian_key.keyring)] kat_input = self.gpg.gen_key_input(separate_keyring=True, **kat) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) kat_key = self.gpg.gen_key(kat_input) kat_fpr = str(kat_key.fingerprint) - self.gpg.options.append('--keyring {}'.format(kat_key.pubring)) + self.gpg.options.append('--keyring {}'.format(kat_key.keyring)) self.gpg.import_keys(kat_key.data) message = """ From ea8c2c5e1f1080be6cf40f1ef4a952f6715d92fe Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 07:14:21 +0000 Subject: [PATCH 142/145] Ugh. Change the unittest paths again. --- gnupg/test/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 1b3c0f2..d394d4c 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -73,7 +73,7 @@ log.setLevel(9) print("Current source directory: %s" % _util._here) print("Current os.cwd directory: %s" % os.getcwd()) -_tests = os.path.join(os.path.join(_util._here, 'gnupg'), 'test') +_tests = os.path.join(_util._here, 'test') _files = os.path.join(_tests, 'files') _tempd = os.path.join(_tests, 'tmp') From db4826f8157cc3680113b3157cd1029f762be074 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 07:14:48 +0000 Subject: [PATCH 143/145] Fix an error in a unittest due to attempt to write class obj, not str form. --- gnupg/test/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index d394d4c..d9eb14a 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -530,7 +530,7 @@ class GPGTestCase(unittest.TestCase): keyfile = os.path.join(self.gpg._keys_dir, 'test_key_3.pub') log.debug("Storing downloaded key as %s" % keyfile) with open(keyfile, 'w') as fh: - fh.write(key) + fh.write(str(key)) self.assertTrue(os.path.isfile(keyfile)) def test_import_and_export(self): From 0ca63524574e005f15a45e0b67b86fa0b3f21901 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 07:15:31 +0000 Subject: [PATCH 144/145] Add check on recv-key size after writing it to a file. --- gnupg/test/test_gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index d9eb14a..5d934a8 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -532,6 +532,7 @@ class GPGTestCase(unittest.TestCase): with open(keyfile, 'w') as fh: fh.write(str(key)) self.assertTrue(os.path.isfile(keyfile)) + self.assertGreater(os.stat(keyfile).st_size, 0) def test_import_and_export(self): """Test that key import and export works.""" From ab54c7b71e2ea619fb9d729da766fb620853fbeb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 2 Jul 2013 07:17:51 +0000 Subject: [PATCH 145/145] Make detached binary sig unittest test what it's supposed to. --- gnupg/test/test_gnupg.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 5d934a8..5332e50 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -701,13 +701,20 @@ class GPGTestCase(unittest.TestCase): def test_signature_verification_detached_binary(self): """Test that detached signature verification in binary mode fails.""" key = self.generate_key("Adi Shamir", "rsa.com") - with open(os.path.join(_files, 'cypherpunk_manifesto'), 'rb') as cm: + datafile = os.path.join(_files, 'cypherpunk_manifesto') + with open(datafile, 'rb') as cm: sig = self.gpg.sign(cm, default_key=key.fingerprint, passphrase='adishamir', detach=True, binary=True, clearsign=False) self.assertTrue(sig.data, "File signing should succeed") + with open(datafile+'.sig', 'w') as bs: + bs.write(sig.data) + bs.flush() with self.assertRaises(UnicodeDecodeError): print("SIG=%s" % sig) + with open(datafile+'.sig', 'rb') as fsig: + with open(datafile, 'rb') as fdata: + self.gpg.verify_file(fdata, fsig) def test_deletion(self): """Test that key deletion works."""