Hello community, here is the log from the commit of package python-gnupg for openSUSE:Factory checked in at 2015-05-10 10:46:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-gnupg (Old) and /work/SRC/openSUSE:Factory/.python-gnupg.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-gnupg" Changes: -------- --- /work/SRC/openSUSE:Factory/python-gnupg/python-gnupg.changes 2013-12-16 07:09:00.000000000 +0100 +++ /work/SRC/openSUSE:Factory/.python-gnupg.new/python-gnupg.changes 2015-05-10 10:46:24.000000000 +0200 @@ -1,0 +2,44 @@ +Wed May 6 12:08:06 UTC 2015 - benoit.mo...@gmx.fr + +- update to version 0.3.7: + * Added an output keyword parameter to the sign and sign_file + methods, to allow writing the signature to a file + * Allowed specifying True for the sign keyword parameter, which + allows use of the default key for signing and avoids having to + specify a key id when it's desired to use the default + * Used a uniform approach with subprocess on Windows and POSIX: + shell=True is not used on either + * When signing/verifying, the status is updated to reflect any + expired or revoked keys or signatures + * Handled 'NOTATION_NAME' and 'NOTATION_DATA' during verification + * Fixed #1, #16, #18, #20: Quoting approach changed, since now + shell=False + * Fixed #14: Handled 'NEED_PASSPHRASE_PIN' message + * Fixed #8: Added a scan_keys method to allow scanning of keys + without the need to import into a keyring + * Fixed #5: Added '0x' prefix when searching for keys + * Fixed #4: Handled 'PROGRESS' message during encryption + * Fixed #3: Changed default encoding to Latin-1 + * Fixed #2: Raised ValueError if no recipients were specified for + an asymmetric encryption request + * Handled 'UNEXPECTED' message during verification + * Replaced old range(len(X)) idiom with enumerate() + * Refactored ListKeys / SearchKeys classes to maximise use of + common functions + * Fixed GC94: Added export-minimal and armor options when + exporting keys +- additional changes from version 0.3.6: + * Fixed GC82: Enabled fast random tests on gpg as well as gpg2 + * Fixed GC85: Avoided deleting temporary file to preserve its + permissions + * Fixed GC87: Avoided writing passphrase to log + * Fixed GC95: Added verify_data() method to allow verification of + signatures in memory + * Fixed GC96: Regularised end-of-line characters + * Fixed GC98: Rectified problems with earlier fix for shell + injection +- point the source URL to pypi +- fix end of line of README.rst +- rename LICENSE.txt and README.rst to follow upstream + +------------------------------------------------------------------- Old: ---- python-gnupg-0.3.5.tar.gz New: ---- python-gnupg-0.3.7.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-gnupg.spec ++++++ --- /var/tmp/diff_new_pack.Exeb3E/_old 2015-05-10 10:46:24.000000000 +0200 +++ /var/tmp/diff_new_pack.Exeb3E/_new 2015-05-10 10:46:24.000000000 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-gnupg # -# Copyright (c) 2013 SUSE LINUX Products GmbH, Nuernberg, Germany. +# Copyright (c) 2015 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,13 +17,13 @@ Name: python-gnupg -Version: 0.3.5 +Version: 0.3.7 Release: 0 Url: http://code.google.com/p/python-gnupg/ Summary: A wrapper for the Gnu Privacy Guard (GPG or GnuPG) License: BSD-3-Clause Group: Development/Languages/Python -Source: python-gnupg-%{version}.tar.gz +Source: https://pypi.python.org/packages/source/p/python-gnupg/python-gnupg-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: python-devel %if 0%{?suse_version} && 0%{?suse_version} <= 1110 @@ -39,6 +39,8 @@ %prep %setup -q -n python-gnupg-%{version} +# fix end of line +sed -i 's/\r//' README.rst %build python setup.py build @@ -48,7 +50,7 @@ %files %defattr(-,root,root,-) -%doc LICENSE README +%doc LICENSE.txt README.rst %{python_sitelib}/* %changelog ++++++ python-gnupg-0.3.5.tar.gz -> python-gnupg-0.3.7.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/LICENSE new/python-gnupg-0.3.7/LICENSE --- old/python-gnupg-0.3.5/LICENSE 2013-02-08 15:09:00.000000000 +0100 +++ new/python-gnupg-0.3.7/LICENSE 1970-01-01 01:00:00.000000000 +0100 @@ -1,26 +0,0 @@ -Copyright (c) 2008-2013 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. - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/LICENSE.txt new/python-gnupg-0.3.7/LICENSE.txt --- old/python-gnupg-0.3.5/LICENSE.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/python-gnupg-0.3.7/LICENSE.txt 2014-02-05 23:03:58.000000000 +0100 @@ -0,0 +1,26 @@ +Copyright (c) 2008-2014 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. + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/PKG-INFO new/python-gnupg-0.3.7/PKG-INFO --- old/python-gnupg-0.3.5/PKG-INFO 2013-08-30 19:11:27.000000000 +0200 +++ new/python-gnupg-0.3.7/PKG-INFO 2014-12-07 20:45:57.000000000 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 1.0 Name: python-gnupg -Version: 0.3.5 +Version: 0.3.7 Summary: A wrapper for the Gnu Privacy Guard (GPG or GnuPG) Home-page: http://packages.python.org/python-gnupg/index.html Author: Vinay Sajip Author-email: vinay_sa...@red-dove.com -License: Copyright (C) 2008-2013 by Vinay Sajip. All Rights Reserved. See LICENSE for license. -Download-URL: http://python-gnupg.googlecode.com/files/python-gnupg-0.3.5.tar.gz +License: Copyright (C) 2008-2014 by Vinay Sajip. All Rights Reserved. See LICENSE.txt for license. +Download-URL: https://pypi.python.org/packages/source/p/python-gnupg/python-gnupg-0.3.7.tar.gz 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.4 or greater. Platform: No particular restrictions Classifier: Development Status :: 5 - Production/Stable @@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 2.5 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: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/README new/python-gnupg-0.3.7/README --- old/python-gnupg-0.3.5/README 2009-07-03 23:39:13.000000000 +0200 +++ new/python-gnupg-0.3.7/README 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -To install this package from a source distribution, do the following. - -1. Extract all the files in the distribution archive to some directory on your system. -2. In that directory, run "python setup.py install". -3. Optionally, run "python test_gnupg.py" to ensure that the package is working as expected. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/README.rst new/python-gnupg-0.3.7/README.rst --- old/python-gnupg-0.3.5/README.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/python-gnupg-0.3.7/README.rst 2014-12-07 18:50:10.000000000 +0100 @@ -0,0 +1,276 @@ +What is it? +=========== + +The GNU Privacy Guard (gpg, or gpg.exe on Windows) is a command-line program +which provides support for programmatic access via spawning a separate process +to run it and then communicating with that process from your program. + +This project, ``python-gnupg``, implements a Python library which takes care +of the internal details and allows its users to generate and manage keys, +encrypt and decrypt data, and sign and verify messages. + +Installation +============ + +Installing from PyPI +-------------------- + +You can install this package from the Python Package Index (pyPI) by running:: + + pip install python-gnupg + + +Installing from a source distribution archive +--------------------------------------------- +To install this package from a source distribution archive, do the following: + +1. Extract all the files in the distribution archive to some directory on your + system. +2. In that directory, run ``python setup.py install``. +3. Optionally, run ``python test_gnupg.py`` to ensure that the package is + working as expected. + +Credits +======= + +* The developers of the GNU Privacy Guard. +* The original version of this module was developed by Andrew Kuchling. +* It was improved by Richard Jones. +* It was further improved by Steve Traugott. + +The present incarnation, based on the earlier versions, uses the ``subprocess`` +module and so works on Windows as well as Unix/Linux platforms. It's not, +however, 100% backwards-compatible with earlier incarnations. + +Change log +========== + +N.B: GCnn refers to an issue nn on Google Code. + +0.3.8 (future) +-------------- + +Released: Not yet + +0.3.7 +----- + +Released: 2014-12-07 + +* Added an ``output`` keyword parameter to the ``sign`` and + ``sign_file`` methods, to allow writing the signature to a file. + Thanks to Jannis Leidel for the patch. + +* Allowed specifying ``True`` for the ``sign`` keyword parameter, + which allows use of the default key for signing and avoids having to + specify a key id when it's desired to use the default. Thanks to + Fabian Beutel for the patch. + +* Used a uniform approach with subprocess on Windows and POSIX: shell=True + is not used on either. + +* When signing/verifying, the status is updated to reflect any expired or + revoked keys or signatures. + +* Handled 'NOTATION_NAME' and 'NOTATION_DATA' during verification. + +* Fixed #1, #16, #18, #20: Quoting approach changed, since now shell=False. + +* Fixed #14: Handled 'NEED_PASSPHRASE_PIN' message. + +* Fixed #8: Added a scan_keys method to allow scanning of keys without the + need to import into a keyring. Thanks to Venzen Khaosan for the suggestion. + +* Fixed #5: Added '0x' prefix when searching for keys. Thanks to Aaron Toponce + for the report. + +* Fixed #4: Handled 'PROGRESS' message during encryption. Thanks to Daniel + Mills for the report. + +* Fixed #3: Changed default encoding to Latin-1. + +* Fixed #2: Raised ValueError if no recipients were specified + for an asymmetric encryption request. + +* Handled 'UNEXPECTED' message during verification. Thanks to + David Andersen for the patch. + +* Replaced old range(len(X)) idiom with enumerate(). + +* Refactored ``ListKeys`` / ``SearchKeys`` classes to maximise use of common + functions. + +* Fixed GC94: Added ``export-minimal`` and ``armor`` options when exporting + keys. This addition was inadvertently left out of 0.3.6. + +0.3.6 +----- + +Released: 2014-02-05 + +* Fixed GC82: Enabled fast random tests on gpg as well as gpg2. +* Fixed GC85: Avoided deleting temporary file to preserve its permissions. +* Fixed GC87: Avoided writing passphrase to log. +* Fixed GC95: Added ``verify_data()`` method to allow verification of + signatures in memory. +* Fixed GC96: Regularised end-of-line characters. +* Fixed GC98: Rectified problems with earlier fix for shell injection. + +0.3.5 +----- + +Released: 2013-08-30 + +* Added improved shell quoting to guard against shell injection. +* Fixed GC76: Added ``search_keys()`` and ``send_keys()`` methods. +* Fixed GC77: Allowed specifying a symmetric cipher algorithm. +* Fixed GC78: Fell back to utf-8 encoding when no other could be determined. +* Fixed GC79: Default key length is now 2048 bits. +* Fixed GC80: Removed the Name-Comment default in key generation. + +0.3.4 +----- + +Released: 2013-06-05 + +* Fixed GC65: Fixed encoding exception when getting version. +* Fixed GC66: Now accepts sets and frozensets where appropriate. +* Fixed GC67: Hash algorithm now captured in sign result. +* Fixed GC68: Added support for ``--secret-keyring``. +* Fixed GC70: Added support for multiple keyrings. + +0.3.3 +----- + +Released: 2013-03-11 + +* Fixed GC57: Handled control characters in ``list_keys()``. +* Fixed GC61: Enabled fast random for testing. +* Fixed GC62: Handled ``KEYEXPIRED`` status. +* Fixed GC63: Handled ``NO_SGNR`` status. + +0.3.2 +----- + +Released: 2013-01-17 + +* Fixed GC56: Disallowed blank values in key generation. +* Fixed GC57: Handled colons and other characters in ``list_keys()``. +* Fixed GC59/GC60: Handled ``INV_SGNR`` status during verification and removed + calls requiring interactive password input from doctests. + +0.3.1 +----- + +Released: 2012-09-01 + +* Fixed GC45: Allowed additional arguments to gpg executable. +* Fixed GC50: Used latin-1 encoding in tests when it's known to be required. +* Fixed GC51: Test now returns non-zero exit status on test failure. +* Fixed GC53: Now handles ``INV_SGNR`` and ``KEY_NOT_CREATED`` statuses. +* Fixed GC55: Verification and decryption now return trust level of signer in + integer and text form. + +0.3.0 +----- + +Released: 2012-05-12 + +* Fixed GC49: Reinstated Yann Leboulanger's change to support subkeys + (accidentally left out in 0.2.7). + +0.2.9 +----- + +Released: 2012-03-29 + +* Fixed GC36: Now handles ``CARDCTRL`` and ``POLICY_URL`` messages. +* Fixed GC40: Now handles ``DECRYPTION_INFO``, ``DECRYPTION_FAILED`` and + ``DECRYPTION_OKAY`` messages. +* The ``random_binary_data file`` is no longer shipped, but constructed by the + test suite if needed. + +0.2.8 +----- + +Released: 2011-09-02 + +* Fixed GC29: Now handles ``IMPORT_RES`` while verifying. +* Fixed GC30: Fixed an encoding problem. +* Fixed GC33: Quoted arguments for added safety. + +0.2.7 +----- + +Released: 2011-04-10 + +* Fixed GC24: License is clarified as BSD. +* Fixed GC25: Incorporated Daniel Folkinshteyn's changes. +* Fixed GC26: Incorporated Yann Leboulanger's subkey change. +* Fixed GC27: Incorporated hysterix's support for symmetric encryption. +* Did some internal cleanups of Unicode handling. + +0.2.6 +----- + +Released: 2011-01-25 + +* Fixed GC14: Should be able to accept passphrases from GPG-Agent. +* Fixed GC19: Should be able to create a detached signature. +* Fixed GC21/GC23: Better handling of less common responses from GPG. + +0.2.5 +----- + +Released: 2010-10-13 + +* Fixed GC11/GC16: Detached signatures can now be created. +* Fixed GC3: Detached signatures can be verified. +* Fixed GC12: Better support for RSA and IDEA. +* Fixed GC15/GC17: Better support for non-ASCII input. + +0.2.4 +----- + +Released: 2010-03-01 + +* Fixed GC9: Now allows encryption without armor and the ability to encrypt + and decrypt directly to/from files. + +0.2.3 +----- + +Released: 2010-01-07 + +* Fixed GC7: Made sending data to process threaded and added a test case. + With a test data file used by the test case, the archive size has gone up + to 5MB (the size of the test file). + +0.2.2 +----- + +Released: 2009-10-06 + +* Fixed GC5/GC6: Added ``--batch`` when specifying ``--passphrase-fd`` and + changed the name of the distribution file to add the ``python-`` prefix. + +0.2.1 +----- + +Released: 2009-08-07 + +* Fixed GC2: Added ``handle_status()`` method to the ``ListKeys`` class. + +0.2.0 +----- + +Released: 2009-07-16 + +* Various changes made to support Python 3.0. + +0.1.0 +----- + +Released: 2009-07-04 + +* Initial release. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/gnupg.py new/python-gnupg-0.3.7/gnupg.py --- old/python-gnupg-0.3.5/gnupg.py 2013-08-30 19:10:36.000000000 +0200 +++ new/python-gnupg-0.3.7/gnupg.py 2014-12-07 19:46:19.000000000 +0100 @@ -27,15 +27,14 @@ and so does not work on Windows). Renamed to gnupg.py to avoid confusion with the previous versions. -Modifications Copyright (C) 2008-2013 Vinay Sajip. All rights reserved. +Modifications Copyright (C) 2008-2014 Vinay Sajip. All rights reserved. A unittest harness (test_gnupg.py) has also been added. """ -import locale -__version__ = "0.3.5" +__version__ = "0.3.7" __author__ = "Vinay Sajip" -__date__ = "$30-Aug-2013 18:10:36$" +__date__ = "$07-Dec-2014 18:46:17$" try: from io import StringIO @@ -103,16 +102,19 @@ raise TypeError('Expected string type, got %s' % type(s)) if not s: result = "''" - elif len(s) >= 2 and (s[0], s[-1]) == ("'", "'"): - result = '"%s"' % s.replace('"', r'\"') elif not UNSAFE.search(s): result = s else: - result = "'%s'" % s.replace("'", "'\"'\"'") + result = "'%s'" % s.replace("'", r"'\''") return result # end of sarge code +# Now that we use shell=False, we shouldn't need to quote arguments. +# Use no_quote instead of shell_quote to remind us of where quoting +# was needed. +def no_quote(s): + return s def _copy_data(instream, outstream): # Copy one stream to another @@ -153,11 +155,19 @@ passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) - logger.debug("Wrote passphrase: %r", passphrase) + logger.debug('Wrote passphrase') def _is_sequence(instance): return isinstance(instance, (list, tuple, set, frozenset)) +def _make_memory_stream(s): + try: + from io import BytesIO + rv = BytesIO(s) + except ImportError: + rv = StringIO(s) + return rv + def _make_binary_stream(s, encoding): if _py3k: if isinstance(s, str): @@ -165,12 +175,7 @@ else: if type(s) is not str: s = s.encode(encoding) - try: - from io import BytesIO - rv = BytesIO(s) - except ImportError: - rv = StringIO(s) - return rv + return _make_memory_stream(s) class Verify(object): "Handle status messages for --verify" @@ -216,7 +221,7 @@ "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", "DECRYPTION_OKAY", "INV_SGNR", "FILE_START", "FILE_ERROR", "FILE_DONE", "PKA_TRUST_GOOD", "PKA_TRUST_BAD", "BADMDC", - "GOODMDC", "NO_SGNR"): + "GOODMDC", "NO_SGNR", "NOTATION_NAME", "NOTATION_DATA"): pass elif key == "BADSIG": self.valid = False @@ -271,6 +276,10 @@ else: self.key_status = 'signing key was revoked' self.status = self.key_status + elif key == "UNEXPECTED": + self.valid = False + self.key_id = value + self.status = 'unexpected data' else: raise ValueError("Unknown status message: %r" % key) @@ -339,8 +348,8 @@ '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])) + for i, count in enumerate(self.counts): + setattr(self, count, int(import_res[i])) elif key == "KEYEXPIRED": self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Key expired'}) @@ -374,7 +383,46 @@ def handle_status(self, key, value): logger.debug('SendResult: %s: %s', key, value) -class ListKeys(list): +class SearchKeys(list): + ''' Handle status messages for --search-keys. + + Handle pub and uid (relating the latter to the former). + + Don't care about the rest + ''' + + UID_INDEX = 1 + FIELDS = 'type keyid algo length date expires'.split() + + def __init__(self, gpg): + self.gpg = gpg + self.curkey = None + self.fingerprints = [] + self.uids = [] + + def get_fields(self, args): + result = {} + for i, var in enumerate(self.FIELDS): + result[var] = args[i] + result['uids'] = [] + return result + + def pub(self, args): + self.curkey = curkey = self.get_fields(args) + self.append(curkey) + + def uid(self, args): + uid = args[self.UID_INDEX] + uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) + for k, v in BASIC_ESCAPES.items(): + uid = uid.replace(k, v) + self.curkey['uids'].append(uid) + self.uids.append(uid) + + def handle_status(self, key, value): + pass + +class ListKeys(SearchKeys): ''' Handle status messages for --list-keys. Handle pub and uid (relating the latter to the former). @@ -391,25 +439,17 @@ grp = reserved for gpgsm rvk = revocation key ''' - def __init__(self, gpg): - self.gpg = gpg - self.curkey = None - self.fingerprints = [] - self.uids = [] + + UID_INDEX = 9 + FIELDS = 'type trust length algo keyid date expires dummy ownertrust uid'.split() 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) + self.curkey = curkey = self.get_fields(args) + if curkey['uid']: + curkey['uids'].append(curkey['uid']) + del curkey['uid'] + curkey['subkeys'] = [] + self.append(curkey) pub = sec = key @@ -417,56 +457,34 @@ 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) - for k, v in BASIC_ESCAPES.items(): - uid = uid.replace(k, v) - 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 SearchKeys(list): - ''' Handle status messages for --search-keys. +class ScanKeys(ListKeys): + ''' Handle status messages for --with-fingerprint.''' - Handle pub and uid (relating the latter to the former). + def sub(self, args): + # --with-fingerprint --with-colons somehow outputs fewer colons, + # use the last value args[-1] instead of args[11] + subkey = [args[4], args[-1]] + self.curkey['subkeys'].append(subkey) - Don't care about the rest - ''' - def __init__(self, gpg): - self.gpg = gpg - self.curkey = None - self.fingerprints = [] - self.uids = [] +class TextHandler(object): + def _as_text(self): + return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) - def pub(self, args): - vars = (""" - type keyid algo length date expires - """).split() - self.curkey = {} - for i in range(len(vars)): - self.curkey[vars[i]] = args[i] - self.curkey['uids'] = [] - self.append(self.curkey) + if _py3k: + __str__ = _as_text + else: + __unicode__ = _as_text - def uid(self, args): - uid = args[1] - uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) - for k, v in BASIC_ESCAPES.items(): - uid = uid.replace(k, v) - self.curkey['uids'].append(uid) - self.uids.append(uid) + def __str__(self): + return self.data - def handle_status(self, key, value): - pass -class Crypt(Verify): +class Crypt(Verify, TextHandler): "Handle status messages for --encrypt and --decrypt" def __init__(self, gpg): Verify.__init__(self, gpg) @@ -480,19 +498,16 @@ __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", + "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", "PROGRESS", "CARDCTRL", "BADMDC", "SC_OP_FAILURE", "SC_OP_SUCCESS"): # 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"): + "KEY_NOT_CREATED", "NEED_PASSPHRASE_PIN"): self.status = key.replace("_", " ").lower() elif key == "NEED_PASSPHRASE_SYM": self.status = 'need symmetric passphrase' @@ -569,7 +584,7 @@ __bool__ = __nonzero__ -class Sign(object): +class Sign(TextHandler): "Handle status messages for --sign" def __init__(self, gpg): self.gpg = gpg @@ -582,15 +597,16 @@ __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", - "KEYEXPIRED", "SIGEXPIRED", "KEYREVOKED", "NO_SGNR", - "MISSING_PASSPHRASE", "SC_OP_FAILURE", "SC_OP_SUCCESS"): + "NO_SGNR", "MISSING_PASSPHRASE", "NEED_PASSPHRASE_PIN", + "SC_OP_FAILURE", "SC_OP_SUCCESS"): pass + elif key in ("KEYEXPIRED", "SIGEXPIRED"): + self.status = 'key expired' + elif key == "KEYREVOKED": + self.status = 'key revoked' elif key == "SIG_CREATED": (self.type, algo, self.hash_algo, cls, @@ -599,7 +615,8 @@ else: raise ValueError("Unknown status message: %r" % key) -VERSION_RE = re.compile(r'gpg \(GnuPG\) (\d+(\.\d+)*)'.encode('utf-8'), re.I) +VERSION_RE = re.compile(r'gpg \(GnuPG\) (\d+(\.\d+)*)'.encode('ascii'), re.I) +HEX_DIGITS_RE = re.compile(r'[0-9a-f]+$', re.I) class GPG(object): @@ -612,6 +629,7 @@ 'import': ImportResult, 'send': SendResult, 'list': ListKeys, + 'scan': ScanKeys, 'search': SearchKeys, 'sign': Sign, 'verify': Verify, @@ -652,13 +670,11 @@ if isinstance(options, str): options = [options] self.options = options - self.encoding = locale.getpreferredencoding() - if self.encoding is None: # This happens on Jython! - self.encoding = sys.stdin.encoding - if self.encoding is None: - logger.warning('No encoding found via locale.getpreferredencoding ' - 'or sys.stdin.encoding, defaulting to utf-8.') - self.encoding = 'utf-8' + # Changed in 0.3.7 to use Latin-1 encoding rather than + # locale.getpreferredencoding falling back to sys.stdin.encoding + # falling back to utf-8, because gpg itself uses latin-1 as the default + # encoding. + self.encoding = 'latin-1' if gnupghome and not os.path.isdir(self.gnupghome): os.makedirs(self.gnupghome,0x1C0) p = self._open_subprocess(["--version"]) @@ -671,7 +687,7 @@ if not m: self.version = None else: - dot = '.'.encode('utf-8') + dot = '.'.encode('ascii') self.version = tuple([int(s) for s in m.groups()[0].split(dot)]) def make_args(self, args, passphrase): @@ -680,18 +696,18 @@ 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'] + cmd = [self.gpgbinary, '--status-fd', '2', '--no-tty'] if self.gnupghome: - cmd.append('--homedir %s' % shell_quote(self.gnupghome)) + cmd.extend(['--homedir', no_quote(self.gnupghome)]) if self.keyring: cmd.append('--no-default-keyring') for fn in self.keyring: - cmd.append('--keyring %s' % shell_quote(fn)) + cmd.extend(['--keyring', no_quote(fn)]) if self.secret_keyring: for fn in self.secret_keyring: - cmd.append('--secret-keyring %s' % shell_quote(fn)) + cmd.extend(['--secret-keyring', no_quote(fn)]) if passphrase: - cmd.append('--batch --passphrase-fd 0') + cmd.extend(['--batch', '--passphrase-fd', '0']) if self.use_agent: cmd.append('--use-agent') if self.options: @@ -702,11 +718,12 @@ def _open_subprocess(self, args, 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)) + cmd = self.make_args(args, passphrase) if self.verbose: - print(cmd) + pcmd = ' '.join(cmd) + print(pcmd) logger.debug("%s", cmd) - return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) + return Popen(cmd, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE) def _read_response(self, stream, result): # Internal method: reads all the stderr output from GPG, taking notice @@ -808,8 +825,15 @@ f.close() return result + def set_output_without_confirmation(self, args, output): + "If writing to a file which exists, avoid a confirmation message." + if os.path.exists(output): + # We need to avoid an overwrite confirmation message + args.extend(['--batch', '--yes']) + args.extend(['--output', output]) + def sign_file(self, file, keyid=None, passphrase=None, clearsign=True, - detach=False, binary=False): + detach=False, binary=False, output=None): """sign file""" logger.debug("sign_file: %s", file) if binary: @@ -823,7 +847,10 @@ elif clearsign: args.append("--clearsign") if keyid: - args.append('--default-key %s' % shell_quote(keyid)) + args.extend(['--default-key', no_quote(keyid)]) + if output: # write the output to a file with the specified name + self.set_output_without_confirmation(args, output) + 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. @@ -875,8 +902,8 @@ logger.debug('Wrote to temp file: %r', s) os.write(fd, s) os.close(fd) - args.append(shell_quote(fn)) - args.append(shell_quote(data_filename)) + args.append(no_quote(fn)) + args.append(no_quote(data_filename)) try: p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) @@ -884,6 +911,15 @@ os.unlink(fn) return result + def verify_data(self, sig_filename, data): + "Verify the signature in sig_filename against data in memory" + logger.debug('verify_data: %r, %r ...', sig_filename, data[:16]) + result = self.result_map['verify'](self) + args = ['--verify', no_quote(sig_filename), '-'] + stream = _make_memory_stream(data) + self._handle_io(args, stream, result, binary=True) + return result + # # KEY MANAGEMENT # @@ -956,8 +992,8 @@ logger.debug('recv_keys: %r', keyids) data = _make_binary_stream("", self.encoding) #data = "" - args = ['--keyserver', shell_quote(keyserver), '--recv-keys'] - args.extend([shell_quote(k) for k in keyids]) + args = ['--keyserver', no_quote(keyserver), '--recv-keys'] + args.extend([no_quote(k) for k in keyids]) self._handle_io(args, data, result, binary=True) logger.debug('recv_keys result: %r', result.__dict__) data.close() @@ -973,8 +1009,8 @@ logger.debug('send_keys: %r', keyids) data = _make_binary_stream('', self.encoding) #data = "" - args = ['--keyserver', shell_quote(keyserver), '--send-keys'] - args.extend([shell_quote(k) for k in keyids]) + args = ['--keyserver', no_quote(keyserver), '--send-keys'] + args.extend([no_quote(k) for k in keyids]) self._handle_io(args, data, result, binary=True) logger.debug('send_keys result: %r', result.__dict__) data.close() @@ -985,25 +1021,31 @@ if secret: which='secret-key' if _is_sequence(fingerprints): - fingerprints = ' '.join([shell_quote(s) for s in fingerprints]) + fingerprints = [no_quote(s) for s in fingerprints] else: - fingerprints = shell_quote(fingerprints) - args = ['--batch --delete-%s %s' % (which, fingerprints)] + fingerprints = [no_quote(fingerprints)] + args = ['--batch', '--delete-%s' % which] + args.extend(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): + def export_keys(self, keyids, secret=False, armor=True, minimal=False): "export the indicated keys. 'keyid' is anything gpg accepts" which='' if secret: which='-secret-key' if _is_sequence(keyids): - keyids = ' '.join([shell_quote(k) for k in keyids]) + keyids = [no_quote(k) for k in keyids] else: - keyids = shell_quote(keyids) - args = ['--armor --export%s %s' % (which, keyids)] + keyids = [no_quote(keyids)] + args = ['--export%s' % which] + if armor: + args.append('--armor') + if minimal: + args.extend(['--export-options','export-minimal']) + args.extend(keyids) p = self._open_subprocess(args) # gpg --export produces no status-fd output; stdout will be # empty in case of failure @@ -1013,6 +1055,27 @@ logger.debug('export_keys result: %r', result.data) return result.data.decode(self.encoding, self.decode_errors) + def _get_list_output(self, p, kind): + # Get the response information + result = self.result_map[kind](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 list_keys(self, secret=False): """ list the keys currently in the keyring @@ -1033,32 +1096,23 @@ which='keys' if secret: which='secret-keys' - args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which,) - args = [args] + args = ["--list-%s" % which, "--fixed-list-mode", "--fingerprint", + "--with-colons"] p = self._open_subprocess(args) + return self._get_list_output(p, 'list') - # there might be some status thingumy here I should handle... (amk) - # ...nope, unless you care about expired sigs or keys (stevegt) + def scan_keys(self, filename): + """ + List details of an ascii armored or binary key file + without first importing it to the local keyring. - # 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 + The function achieves this by running: + $ gpg --with-fingerprint --with-colons filename + """ + args = ['--with-fingerprint', '--with-colons'] + args.append(no_quote(filename)) + p = self._open_subprocess(args) + return self._get_list_output(p, 'scan') def search_keys(self, query, keyserver='pgp.mit.edu'): """ search keyserver by query (using --search-keys option) @@ -1068,16 +1122,18 @@ >>> gpg = GPG(gnupghome='keys') >>> os.chmod('keys', 0x1C0) >>> result = gpg.search_keys('<vinay_sa...@hotmail.com>') - >>> assert result + >>> assert result, 'Failed using default keyserver' >>> keyserver = 'keyserver.ubuntu.com' >>> result = gpg.search_keys('<vinay_sa...@hotmail.com>', keyserver) - >>> assert result + >>> assert result, 'Failed using keyserver.ubuntu.com' """ - + query = query.strip() + if HEX_DIGITS_RE.match(query): + query = '0x' + query args = ['--fixed-list-mode', '--fingerprint', '--with-colons', - '--keyserver', shell_quote(keyserver), '--search-keys', - shell_quote(query)] + '--keyserver', no_quote(keyserver), '--search-keys', + no_quote(query)] p = self._open_subprocess(args) # Get the response information @@ -1112,7 +1168,7 @@ >>> assert not result """ - args = ["--gen-key --batch"] + 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) @@ -1179,24 +1235,26 @@ # such as AES256 args = ['--symmetric'] if symmetric is not True: - args.extend(['--cipher-algo', shell_quote(symmetric)]) + args.extend(['--cipher-algo', no_quote(symmetric)]) # else use the default, currently CAST5 else: - args = ['--encrypt'] + if not recipients: + raise ValueError('No recipients specified with asymmetric ' + 'encryption') if not _is_sequence(recipients): recipients = (recipients,) for recipient in recipients: - args.append('--recipient %s' % shell_quote(recipient)) + args.extend(['--recipient', no_quote(recipient)]) if armor: # create ascii-armored output - 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' % shell_quote(output)) - if sign: - args.append('--sign --default-key %s' % shell_quote(sign)) + self.set_output_without_confirmation(args, output) + if sign is True: + args.append('--sign') + elif sign: + args.extend(['--sign', '--default-key', no_quote(sign)]) if always_trust: - args.append("--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) @@ -1258,13 +1316,10 @@ 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' % shell_quote(output)) + self.set_output_without_confirmation(args, 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/setup.py new/python-gnupg-0.3.7/setup.py --- old/python-gnupg-0.3.5/setup.py 2013-01-23 12:01:38.000000000 +0100 +++ new/python-gnupg-0.3.7/setup.py 2014-11-03 10:47:28.000000000 +0100 @@ -7,7 +7,7 @@ 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.4 or greater.", - license="""Copyright (C) 2008-2013 by Vinay Sajip. All Rights Reserved. See LICENSE for license.""", + license="""Copyright (C) 2008-2014 by Vinay Sajip. All Rights Reserved. See LICENSE.txt for license.""", version=version, author="Vinay Sajip", author_email="vinay_sa...@red-dove.com", @@ -16,7 +16,7 @@ url="http://packages.python.org/python-gnupg/index.html", py_modules=["gnupg"], platforms="No particular restrictions", - download_url="http://python-gnupg.googlecode.com/files/python-gnupg-%s.tar.gz" % version, + download_url="https://pypi.python.org/packages/source/p/python-gnupg/python-gnupg-%s.tar.gz" % version, classifiers=[ 'Development Status :: 5 - Production/Stable', "Intended Audience :: Developers", @@ -28,9 +28,9 @@ "Programming Language :: Python :: 2.5", "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", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules" ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-gnupg-0.3.5/test_gnupg.py new/python-gnupg-0.3.7/test_gnupg.py --- old/python-gnupg-0.3.5/test_gnupg.py 2013-08-30 19:10:57.000000000 +0200 +++ new/python-gnupg-0.3.7/test_gnupg.py 2014-12-07 19:46:41.000000000 +0100 @@ -2,13 +2,14 @@ """ A test harness for gnupg.py. -Copyright (C) 2008-2013 Vinay Sajip. All rights reserved. +Copyright (C) 2008-2014 Vinay Sajip. All rights reserved. """ import doctest import logging import os.path import os import shutil +import stat import sys import tempfile import unittest @@ -16,7 +17,7 @@ import gnupg __author__ = "Vinay Sajip" -__date__ = "$30-Aug-2013 18:10:57$" +__date__ = "$07-Dec-2014 18:46:40$" ALL_TESTS = True @@ -98,9 +99,18 @@ "Not a directory: %s" % hd) shutil.rmtree(hd) self.homedir = hd - self.gpg = gnupg.GPG(gnupghome=hd, gpgbinary=GPGBINARY) - if self.gpg.version and self.gpg.version >= (2,): - self.gpg.options = ['--debug-quick-random'] + self.gpg = gpg = gnupg.GPG(gnupghome=hd, gpgbinary=GPGBINARY) + v = gpg.version + if v: + if v >= (2,): + gpg.options = ['--debug-quick-random'] + else: + gpg.options = ['--quick-random'] + self.test_fn = test_fn = 'random_binary_data' + if not os.path.exists(test_fn): + data_file = open(test_fn, 'wb') + data_file.write(os.urandom(5120 * 1024)) + data_file.close() def test_environment(self): "Test the environment by ensuring that setup worked" @@ -129,18 +139,20 @@ 'Name-Comment': 'A test user', 'Expire-Date': 0, } - if '--debug-quick-random' in (self.gpg.options or []): + options = self.gpg.options or [] + if '--debug-quick-random' in options or '--quick-random' in options: # If using the fake RNG, a key isn't regarded as valid # unless its comment has the text (insecure!) in it. params['Name-Comment'] = 'A test user (insecure!)' params['Name-Real'] = '%s %s' % (first_name, last_name) - params['Name-Email'] = ("%s.%s@%s" % (first_name, last_name, domain)).lower() + params['Name-Email'] = ("%s.%s@%s" % (first_name, last_name, + domain)).lower() if passphrase is None: passphrase = ("%s%s" % (first_name[0], last_name)).lower() params['Passphrase'] = passphrase cmd = self.gpg.gen_key_input(**params) return self.gpg.gen_key(cmd) - + def do_key_generation(self): "Test that key generation succeeds" result = self.generate_key("Barbara", "Brown", "beta.com") @@ -210,7 +222,7 @@ params['key_type'] = 'DSA' cmd = self.gpg.gen_key_input(**params) self.assertTrue('Key-Type: DSA\n' in cmd) - + def test_list_keys_after_generation(self): "Test that after key generation, the generated key is available" self.test_list_keys_initial() @@ -231,6 +243,20 @@ private_keys_2 = gpg.list_keys(secret=True) self.assertEqual(private_keys_2, private_keys) + def test_scan_keys(self): + "Test that external key files can be scanned" + expected = set([ + 'Andrew Able (A test user) <andrew.a...@alpha.com>', + 'Barbara Brown (A test user) <barbara.br...@beta.com>', + 'Charlie Clark (A test user) <charlie.cl...@gamma.com>', + ]) + for fn in ('test_pubring.gpg', 'test_secring.gpg'): + data = self.gpg.scan_keys(fn) + uids = set() + for d in data: + uids.add(d['uids'][0]) + self.assertEqual(uids, expected) + def test_encryption_and_decryption(self): "Test that encryption and decryption works" logger.debug("test_encryption_and_decryption begins") @@ -266,11 +292,12 @@ ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data, str(ddata)) # Test symmetric encryption with non-default cipher - data = "chippy was here" edata = str(gpg.encrypt(data, None, passphrase='bbrown', symmetric='AES256')) ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data, str(ddata)) + # Test that you can't encrypt with no recipients + self.assertRaises(ValueError, self.gpg.encrypt, data, []) def test_import_and_export(self): "Test that key import and export works" @@ -346,11 +373,7 @@ "Fingerprints must match") self.assertEqual(verified.trust_level, verified.TRUST_ULTIMATE) self.assertEqual(verified.trust_text, 'TRUST_ULTIMATE') - if not os.path.exists('random_binary_data'): - data_file = open('random_binary_data', 'wb') - data_file.write(os.urandom(5120 * 1024)) - data_file.close() - data_file = open('random_binary_data', 'rb') + data_file = open(self.test_fn, 'rb') sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, passphrase='aable') data_file.close() @@ -367,7 +390,7 @@ logger.debug("ver: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") - data_file = open('random_binary_data', 'rb') + data_file = open(self.test_fn, 'rb') sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, passphrase='aable', detach=True) data_file.close() @@ -375,7 +398,7 @@ self.assertTrue(sig.hash_algo) try: file = gnupg._make_binary_stream(sig.data, self.gpg.encoding) - verified = self.gpg.verify_file(file, 'random_binary_data') + verified = self.gpg.verify_file(file, self.test_fn) except UnicodeDecodeError: #happens in Python 2.6 from io import BytesIO verified = self.gpg.verify_file(BytesIO(sig.data)) @@ -384,8 +407,52 @@ logger.debug("ver: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") + # Test in-memory verification + data_file = open(self.test_fn, 'rb') + data = data_file.read() + data_file.close() + fd, fn = tempfile.mkstemp() + os.write(fd, sig.data) + os.close(fd) + try: + verified = self.gpg.verify_data(fn, data) + finally: + os.unlink(fn) + if key.fingerprint != verified.fingerprint: + logger.debug("key: %r", key.fingerprint) + logger.debug("ver: %r", verified.fingerprint) + self.assertEqual(key.fingerprint, verified.fingerprint, + "Fingerprints must match") logger.debug("test_signature_verification ends") + def test_signature_file(self): + "Test that signing and verification works via the GPG output" + logger.debug("test_signature_file begins") + key = self.generate_key("Andrew", "Able", "alpha.com") + data_file = open(self.test_fn, 'rb') + sig_file = self.test_fn + '.asc' + sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, + passphrase='aable', detach=True, + output=sig_file) + data_file.close() + self.assertTrue(sig, "File signing should succeed") + self.assertTrue(sig.hash_algo) + self.assertTrue(os.path.exists(sig_file)) + # Test in-memory verification + data_file = open(self.test_fn, 'rb') + data = data_file.read() + data_file.close() + try: + verified = self.gpg.verify_data(sig_file, data) + finally: + os.unlink(sig_file) + if key.fingerprint != verified.fingerprint: + logger.debug("key: %r", key.fingerprint) + logger.debug("ver: %r", verified.fingerprint) + self.assertEqual(key.fingerprint, verified.fingerprint, + "Fingerprints must match") + logger.debug("test_signature_file ends") + def test_deletion(self): "Test that key deletion works" logger.debug("test_deletion begins") @@ -401,7 +468,7 @@ def test_nogpg(self): "Test that absence of gpg is handled correctly" - self.assertRaises(ValueError, gnupg.GPG, gnupghome=self.homedir, + self.assertRaises(OSError, gnupg.GPG, gnupghome=self.homedir, gpgbinary='frob') def test_make_args(self): @@ -411,14 +478,16 @@ self.assertTrue(len(args) > 4) self.assertEqual(args[-4:], ['--foo', '--bar', 'a', 'b']) - def test_file_encryption_and_decryption(self): - "Test that encryption/decryption to/from file works" - logger.debug("test_file_encryption_and_decryption begins") - encfno, encfname = tempfile.mkstemp() - decfno, decfname = tempfile.mkstemp() - # On Windows, if the handles aren't closed, the files can't be deleted - os.close(encfno) - os.close(decfno) + def do_file_encryption_and_decryption(self, encfname, decfname): + "Do the actual encryption.decryptin test using given filenames" + mode = None + if os.name == 'posix': + # pick a mode that won't be already in effect via umask + if os.path.exists(encfname) and os.path.exists(decfname): + mode = os.stat(encfname).st_mode | stat.S_IXUSR + os.chmod(encfname, mode) + # assume same for decfname + os.chmod(decfname, mode) logger.debug('Encrypting to: %r', encfname) logger.debug('Decrypting to: %r', decfname) try: @@ -448,10 +517,36 @@ self.assertEqual(data, ddata, "Round-trip must work") finally: for fn in (encfname, decfname): + if os.name == 'posix' and mode is not None: + # Check that the file wasn't deleted, and that the + # mode bits we set are still in effect + self.assertEqual(os.stat(fn).st_mode, mode) if os.path.exists(fn): os.remove(fn) + + def test_file_encryption_and_decryption(self): + "Test that encryption/decryption to/from file works" + logger.debug("test_file_encryption_and_decryption begins") + encfno, encfname = tempfile.mkstemp() + decfno, decfname = tempfile.mkstemp() + # On Windows, if the handles aren't closed, the files can't be deleted + os.close(encfno) + os.close(decfno) + self.do_file_encryption_and_decryption(encfname, decfname) logger.debug("test_file_encryption_and_decryption ends") + def test_filenames_with_spaces(self): # See Issue #16 + "Test that filenames with spaces are correctly handled" + logger.debug("test_filename_with_spaces begins") + d = tempfile.mkdtemp() + try: + encfname = os.path.join(d, 'encrypted file') + decfname = os.path.join(d, 'decrypted file') + self.do_file_encryption_and_decryption(encfname, decfname) + finally: + shutil.rmtree(d) + logger.debug("test_filename_with_spaces ends") + def test_search_keys(self): "Test that searching for keys works" r = self.gpg.search_keys('<vinay_sa...@hotmail.com>') @@ -461,29 +556,71 @@ self.assertTrue(r) self.assertTrue('Vinay Sajip <vinay_sa...@hotmail.com>' in r[0]['uids']) + def test_quote_with_shell(self): + "Test shell quoting with a real shell" + if os.name != 'posix': return + + from subprocess import PIPE, Popen + + workdir = tempfile.mkdtemp() + try: + s = "'\\\"; touch %s/foo #'" % workdir + cmd = 'echo %s' % gnupg.shell_quote(s) + p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + p.communicate() + self.assertEqual(p.returncode, 0) + files = os.listdir(workdir) + self.assertEqual(files, []) + fn = "'ab?'" + cmd = 'touch %s/%s' % (workdir, gnupg.shell_quote(fn)) + p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + p.communicate() + self.assertEqual(p.returncode, 0) + files = os.listdir(workdir) + self.assertEqual(files, ["'ab?'"]) + finally: + shutil.rmtree(workdir) + + def disabled_test_signing_with_uid(self): + "Test that signing with uids works. On hold for now." + logger.debug("test_signing_with_uid begins") + key = self.generate_key("Andrew", "Able", "alpha.com") + uid = self.gpg.list_keys(True)[-1]['uids'][0] + try: + signfile = open(self.test_fn,'rb') + signed = self.gpg.sign_file(signfile, keyid=uid, + passphrase='aable', + detach=True) + finally: + signfile.close() + self.assertTrue(signed.data) + logger.debug("test_signing_with_uid ends") TEST_GROUPS = { 'sign' : set(['test_signature_verification']), 'crypt' : set(['test_encryption_and_decryption', - 'test_file_encryption_and_decryption']), + 'test_file_encryption_and_decryption', + 'test_filenames_with_spaces']), 'key' : set(['test_deletion', 'test_import_and_export', 'test_list_keys_after_generation', 'test_key_generation_with_invalid_key_type', 'test_key_generation_with_escapes', 'test_key_generation_with_empty_value', 'test_key_generation_with_colons', - 'test_search_keys']), + 'test_search_keys', 'test_scan_keys']), 'import' : set(['test_import_only']), 'basic' : set(['test_environment', 'test_list_keys_initial', - 'test_nogpg', 'test_make_args']), + 'test_nogpg', 'test_make_args', + 'test_quote_with_shell']), + 'test': set(['test_search_keys']), } def suite(args=None): if args is None: args = sys.argv[1:] - if not args: + if not args or args == ['--no-doctests']: result = unittest.TestLoader().loadTestsFromTestCase(GPGTestCase) - want_doctests = True + want_doctests = not args else: tests = set() want_doctests = False @@ -493,7 +630,7 @@ elif arg == "doc": want_doctests = True else: - print("Ignoring unknown test group %r" % arg) + print("Ignoring unknown test group %r" % arg) result = unittest.TestSuite(list(map(GPGTestCase, tests))) if want_doctests: result.addTest(doctest.DocTestSuite(gnupg))