From 77c6c3d0e53b351aac017c03d0755859519053dd Mon Sep 17 00:00:00 2001 From: Charles Duffy Date: Wed, 22 Oct 2014 07:04:26 -0500 Subject: [PATCH 01/26] Make homedir permissions check optional --- gnupg/_meta.py | 28 ++++++++++++++++------------ gnupg/gnupg.py | 12 ++++++++++-- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 3f7ac5d..2e0052b 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -132,7 +132,7 @@ class GPGBase(object): def __init__(self, binary=None, home=None, keyring=None, secring=None, use_agent=False, default_preference_list=None, - verbose=False, options=None): + ignore_homedir_permissions=False, verbose=False, options=None): """Create a ``GPGBase``. This class is used to set up properties for controlling the behaviour @@ -155,6 +155,7 @@ class GPGBase(object): :ivar str secring: The filename in **homedir** to use as the keyring file for secret keys. """ + self.ignore_homedir_permissions = ignore_homedir_permissions self.binary = _util._find_binary(binary) self.homedir = os.path.expanduser(home) if home else _util._conf pub = _parsers._fix_unsafe(keyring) if keyring else 'pubring.gpg' @@ -398,18 +399,21 @@ class GPGBase(object): 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(str(ae)) - raise RuntimeError(str(ae)) - else: - log.info("Setting homedir to '%s'" % hd) + if self.ignore_homedir_permissions: self._homedir = hd + else: + 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(str(ae)) + raise RuntimeError(str(ae)) + else: + log.info("Setting homedir to '%s'" % hd) + self._homedir = hd homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7168017..2fde164 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -60,7 +60,7 @@ class GPG(GPGBase): def __init__(self, binary=None, homedir=None, verbose=False, use_agent=False, keyring=None, secring=None, - options=None): + ignore_homedir_permissions=False, options=None): """Initialize a GnuPG process wrapper. :param str binary: Name for GnuPG binary executable. If the absolute @@ -73,6 +73,10 @@ class GPG(GPGBase): and private keyrings. Default is whatever GnuPG defaults to. + :type ignore_homedir_permissions: :obj:`bool` + :param ignore_homedir_permissions: If true, bypass check that homedir + be writable. + :type verbose: :obj:`str` or :obj:`int` or :obj:`bool` :param verbose: String or numeric value to pass to GnuPG's ``--debug-level`` option. See the GnuPG man page for @@ -117,13 +121,16 @@ class GPG(GPGBase): secring=secring, options=options, verbose=verbose, - use_agent=use_agent,) + use_agent=use_agent, + ignore_homedir_permissions=ignore_homedir_permissions, + ) log.info(textwrap.dedent(""" Initialised settings: binary: %s binary version: %s homedir: %s + ignore_homedir_permissions: %s keyring: %s secring: %s default_preference_list: %s @@ -134,6 +141,7 @@ class GPG(GPGBase): """ % (self.binary, self.binary_version, self.homedir, + self.ignore_homedir_permissions, self.keyring, self.secring, self.default_preference_list, From a1c45a6f63b35659b5d9f889b7dc679342e35774 Mon Sep 17 00:00:00 2001 From: Charles Duffy Date: Wed, 22 Oct 2014 08:23:17 -0500 Subject: [PATCH 02/26] Not sufficient to drop bad options; good ones need to be passed through. This code was broken: Half of it required `options` to be a string, and the other half required `options` to be a list (which the tests enforced, but the constructor would silently drop for normal-path initialization). --- gnupg/_meta.py | 4 ++-- gnupg/_parsers.py | 5 ++++- gnupg/test/test_gnupg.py | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 3f7ac5d..34bfc95 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -161,7 +161,7 @@ class GPGBase(object): 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 = _parsers._sanitise(options) if options else None + self.options = list(_parsers._sanitise_list(options)) if options else None #: The version string of our GnuPG binary self.binary_version = '0.0.0' @@ -197,7 +197,7 @@ class GPGBase(object): "'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" + assert isinstance(self.options, list), "options not list" except (AssertionError, AttributeError) as ae: log.error("GPGBase.__init__(): %s" % str(ae)) raise RuntimeError(str(ae)) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 93da10b..8aa8a91 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -367,7 +367,7 @@ def _sanitise(*args): checked += (val + " ") log.debug("_check_option(): No checks for %s" % val) - return checked + return checked.rstrip(' ') is_flag = lambda x: x.startswith('--') @@ -557,6 +557,9 @@ def _get_options_group(group=None): '--list-public-keys', '--list-secret-keys', '--list-sigs', + '--lock-multiple', + '--lock-never', + '--lock-once', '--no-default-keyring', '--no-default-recipient', '--no-emit-version', diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 49f5ba5..2ae305a 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -288,8 +288,8 @@ class GPGTestCase(unittest.TestCase): self.assertTrue(os.path.isabs(self.gpg.binary)) def test_make_args_drop_protected_options(self): - """Test that unsupported gpg options are dropped.""" - self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] + """Test that unsupported gpg options are dropped, and supported ones remain.""" + self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus', '--lock-never'] gpg_binary_path = _util._find_binary('gpg') cmd = self.gpg._make_args(None, False) expected = [gpg_binary_path, @@ -297,7 +297,8 @@ class GPGTestCase(unittest.TestCase): '--homedir "%s"' % self.homedir, '--no-default-keyring --keyring %s' % self.keyring, '--secret-keyring %s' % self.secring, - '--no-use-agent'] + '--no-use-agent', + '--lock-never'] self.assertListEqual(cmd, expected) def test_make_args(self): From 8a7699236ce8810c0fb8e4703253d3888fdd8f3e Mon Sep 17 00:00:00 2001 From: Viral Bajaria Date: Tue, 6 Jan 2015 11:13:46 -0800 Subject: [PATCH 03/26] add output as a valid option --- gnupg/_parsers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index 93da10b..d479509 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -516,6 +516,7 @@ def _get_options_group(group=None): '--import', '--verify', '--verify-files', + '--output', ]) #: These options expect a string. see :func:`_check_preferences`. pref_options = frozenset(['--digest-algo', From f8ccdc5028f2d8d74233abfdb01cb695b59463bb Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Sat, 17 Jan 2015 16:09:39 -0800 Subject: [PATCH 04/26] Fix `GPG.encrypt` for file-like objects `GPG.encrypt_file` was refactored into `GPG.encrypt` in 295d98f, which broke the functionality of `GPG.encrypt_file` for encrypting file-like stream objects such as StringIO, BytesIO, etc. The main difference between `GPG.encrypt_file` and `GPG.encrypt` is that `GPG.encrypt` first converts its `data` argument into a binary stream via `_make_binary_stream`. This is unnecessary when the argument is already a stream, as was often the case in calls to `GPG.encrypt_file`. Additionally, `_make_binary_stream` typically fails when it attempts to encode a stream object, which means it is no longer possible to achieve the functionality of `GPG.encrypt_file` with `GPG.encrypt` after the refactor. This commit only converts `data` to a binary stream if it is not already a stream, re-using `_util._is_stream` to make that determination. --- gnupg/gnupg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7168017..757f3e2 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -952,7 +952,10 @@ generate keys. Please see .. seealso:: :meth:`._encrypt` """ - stream = _make_binary_stream(data, self._encoding) + if _is_stream(data): + stream = data + else: + stream = _make_binary_stream(data, self._encoding) result = self._encrypt(stream, recipients, **kwargs) stream.close() return result From 8c261eba30b6b82e1db8674f8f8e2381a457dc84 Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Tue, 20 Jan 2015 09:22:54 -0800 Subject: [PATCH 05/26] Expand set of classes recognized by `_util._is_stream` Adds additional commonly used stream classes from the standard library to `_util._is_stream`. This means these classes can be used successfully wherever `_is_stream` is used to decide whether or not to encode data throughout the codebase, including in `_encrypt`. --- gnupg/_util.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index fff8c0c..c193494 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -41,6 +41,17 @@ try: except ImportError: from cStringIO import StringIO +# Import the StringIO class from the StringIO module since it is a +# commonly used stream class. It is distinct from either of the +# StringIO's that may be loaded in the above try/except clause, so the +# name is prefixed with an underscore to distinguish it. +from StringIO import StringIO as _StringIO + +# Import the cStringIO module to test for the cStringIO stream types, +# InputType and OutputType. See +# http://stackoverflow.com/questions/14735295/to-check-an-instance-is-stringio +import cStringIO + from . import _logger @@ -350,7 +361,8 @@ def _is_stream(input): :rtype: bool :returns: True if :param:input is a stream, False if otherwise. """ - return isinstance(input, BytesIO) or isinstance(input, StringIO) + return isinstance(input, (BytesIO, StringIO, _StringIO, + cStringIO.InputType, cStringIO.OutputType)) def _is_list_or_tuple(instance): """Check that ``instance`` is a list or tuple. From 6c15f25ee55b96c37b59667e7fcaefbfe246615f Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Tue, 20 Jan 2015 11:18:55 -0800 Subject: [PATCH 06/26] Unit test for encrypting file-like objects --- gnupg/test/test_gnupg.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 49f5ba5..67aee53 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -776,7 +776,47 @@ authentication.""" log.debug("Encrypted: %s" % encrypted) self.assertNotEquals(message, encrypted) + def test_encryption_of_file_like_objects(self): + """Test encryption of file-like objects""" + key = self.generate_key("Craig Gentry", "xorr.ox", + passphrase="craiggentry") + gentry_fpr = str(key.fingerprint) + gentry = self.gpg.export_keys(key.fingerprint) + self.gpg.import_keys(gentry) + + message = """ +In 2010 Riggio and Sicari presented a practical application of homomorphic +encryption to a hybrid wireless sensor/mesh network. The system enables +transparent multi-hop wireless backhauls that are able to perform statistical +analysis of different kinds of data (temperature, humidity, etc.) coming from +a WSN while ensuring both end-to-end encryption and hop-by-hop +authentication.""" + + def _encryption_test_wrapper(stream_type, message): + stream = stream_type(message) + encrypted = str(self.gpg.encrypt(stream, gentry_fpr)) + decrypted = str(self.gpg.decrypt(encrypted, + passphrase="craiggentry")) + self.assertEqual(message, decrypted) + + # Test io.StringIO and io.BytesIO (Python 2.6+) + try: + from io import StringIO, BytesIO + _encryption_test_wrapper(StringIO, unicode(message)) + _encryption_test_wrapper(BytesIO, message) + except ImportError: + pass + + # Test StringIO.StringIO + from StringIO import StringIO + _encryption_test_wrapper(StringIO, message) + + # Test cStringIO.StringIO + from cStringIO import StringIO + _encryption_test_wrapper(StringIO, message) + def test_encryption_alt_encoding(self): + """Test encryption with latin-1 encoding""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") @@ -1146,6 +1186,7 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_string_verification', 'test_signature_string_algorithm_encoding']), 'crypt': set(['test_encryption', + 'test_encryption_of_file_like_objects', 'test_encryption_alt_encoding', 'test_encryption_multi_recipient', 'test_encryption_decryption_multi_recipient', From ceb1c2fbbde3d75368ad793b441c6c160e6fa657 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 9 Mar 2015 06:24:34 +0000 Subject: [PATCH 07/26] Add _STREAMLIKE_TYPES for determining stream-likeness in _is_stream(). * FIXES Python3 problems with various StringIO imports commit 8c261eb from fix for #89 in PR #92. --- gnupg/_util.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index c193494..6d8d3e2 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -35,22 +35,36 @@ import re import string import sys +# These are all the classes which are stream-like; they are used in +# :func:`_is_stream`. +_STREAMLIKE_TYPES = [] + +# These StringIO classes are actually utilised. try: from io import StringIO from io import BytesIO except ImportError: from cStringIO import StringIO +else: + _STREAMLIKE_TYPES.append(BytesIO) + _STREAMLIKE_TYPES.append(StringIO) -# Import the StringIO class from the StringIO module since it is a -# commonly used stream class. It is distinct from either of the -# StringIO's that may be loaded in the above try/except clause, so the -# name is prefixed with an underscore to distinguish it. -from StringIO import StringIO as _StringIO +# The remaining StringIO classes which are imported are used to determine if a +# object is a stream-like in :func:`_is_stream`. +if sys.version_info.major == 2: + # Import the StringIO class from the StringIO module since it is a + # commonly used stream class. It is distinct from either of the + # StringIO's that may be loaded in the above try/except clause, so the + # name is prefixed with an underscore to distinguish it. + from StringIO import StringIO as _StringIO_StringIO + _STREAMLIKE_TYPES.append(_StringIO_StringIO) -# Import the cStringIO module to test for the cStringIO stream types, -# InputType and OutputType. See -# http://stackoverflow.com/questions/14735295/to-check-an-instance-is-stringio -import cStringIO + # Import the cStringIO module to test for the cStringIO stream types, + # InputType and OutputType. See + # http://stackoverflow.com/questions/14735295/to-check-an-instance-is-stringio + import cStringIO as _cStringIO + _STREAMLIKE_TYPES.append(_cStringIO.InputType) + _STREAMLIKE_TYPES.append(_cStringIO.OutputType) from . import _logger @@ -361,8 +375,7 @@ def _is_stream(input): :rtype: bool :returns: True if :param:input is a stream, False if otherwise. """ - return isinstance(input, (BytesIO, StringIO, _StringIO, - cStringIO.InputType, cStringIO.OutputType)) + return isinstance(input, tuple(_STREAMLIKE_TYPES)) def _is_list_or_tuple(instance): """Check that ``instance`` is a list or tuple. From d31d0cf1311576b226229904a4bdbddc94b641db Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 9 Mar 2015 08:12:41 +0000 Subject: [PATCH 08/26] Handle MISSING_PASSPHRASE in _parsers.Sign. * FIXES #91. --- gnupg/_parsers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index cddbc2c..dbdcb79 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -912,6 +912,7 @@ class Sign(object): timestamp = None #: xxx fill me in what = None + status = None def __init__(self, gpg): self._gpg = gpg @@ -934,9 +935,9 @@ class Sign(object): :raises: :exc:`~exceptions.ValueError` if the status message is unknown. """ if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", - "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", - "INV_SGNR", "SIGEXPIRED"): - pass + "GOOD_PASSPHRASE", "MISSING_PASSPHRASE", + "BEGIN_SIGNING", "CARDCTRL", "INV_SGNR", "SIGEXPIRED"): + self.status = key.replace("_", " ").lower() elif key == "SIG_CREATED": (self.sig_type, self.sig_algo, self.sig_hash_algo, self.what, self.timestamp, self.fingerprint) = value.split() @@ -953,6 +954,7 @@ class Sign(object): else: raise ValueError("Unknown status message: %r" % key) + class ListKeys(list): """Handle status messages for --list-keys. From d3e6ae33b43889974b19d070448dcd906a377bc4 Mon Sep 17 00:00:00 2001 From: Thomas Tanner Date: Thu, 14 Nov 2013 23:11:32 +0100 Subject: [PATCH 09/26] no-use-agent is obsolete for GPG2 (cherry picked from commit 19fd35c7232e42a4112c8f18686df1c0407c2d0d) Signed-off-by: Isis Lovecruft * FIXES #96. * CLOSES #96. * CLOSES #46. --- gnupg/_meta.py | 2 +- gnupg/gnupg.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 934eba8..d90da47 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -538,7 +538,7 @@ class GPGBase(object): if passphrase: cmd.append('--batch --passphrase-fd 0') if self.use_agent: cmd.append('--use-agent') - else: cmd.append('--no-use-agent') + elif self.use_agent==False: cmd.append('--no-use-agent') # obsolete for GPG 2.0 # The arguments for debugging and verbosity should be placed into the # cmd list before the options/args in order to resolve Issue #76: diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 6a2fb46..2427c4d 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -161,6 +161,9 @@ class GPG(GPGBase): # fatal error (at least it does with GnuPG>=2.0.0): self.create_trustdb() + # --no-use-agent is obsolete + if not self.use_agent: self.use_agent = None + @functools.wraps(_trust._create_trustdb) def create_trustdb(self): if self.is_gpg2(): From ae5cb33d6348de6afedb6f0dc82c0dd041635db5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 9 Mar 2015 08:33:11 +0000 Subject: [PATCH 10/26] Unset GPG.user_agent if using gpg2 binary. * FIXES #96. --- gnupg/_meta.py | 4 ++-- gnupg/gnupg.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index d90da47..f93bae4 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -537,8 +537,8 @@ class GPGBase(object): if passphrase: cmd.append('--batch --passphrase-fd 0') - if self.use_agent: cmd.append('--use-agent') - elif self.use_agent==False: cmd.append('--no-use-agent') # obsolete for GPG 2.0 + if self.use_agent is True: cmd.append('--use-agent') + elif self.use_agent is False: cmd.append('--no-use-agent') # The arguments for debugging and verbosity should be placed into the # cmd list before the options/args in order to resolve Issue #76: diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 2427c4d..12b46ce 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -161,8 +161,11 @@ class GPG(GPGBase): # fatal error (at least it does with GnuPG>=2.0.0): self.create_trustdb() - # --no-use-agent is obsolete - if not self.use_agent: self.use_agent = None + # The --no-use-agent and --use-agent options were deprecated in GnuPG + # 2.x, so we should set use_agent to None here to avoid having + # GPGBase._make_args() add either one. + if self.is_gpg2(): + self.use_agent = None @functools.wraps(_trust._create_trustdb) def create_trustdb(self): From eb205774fbd8c04b9204fd8d82b4e90de0e6ccf7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 9 Mar 2015 08:50:52 +0000 Subject: [PATCH 11/26] Add support for PINENTRY_LAUNCHED status message. * FIXES #98. --- gnupg/_parsers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index dbdcb79..d305842 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -935,7 +935,7 @@ class Sign(object): :raises: :exc:`~exceptions.ValueError` if the status message is unknown. """ if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", - "GOOD_PASSPHRASE", "MISSING_PASSPHRASE", + "GOOD_PASSPHRASE", "MISSING_PASSPHRASE", "PINENTRY_LAUNCHED", "BEGIN_SIGNING", "CARDCTRL", "INV_SGNR", "SIGEXPIRED"): self.status = key.replace("_", " ").lower() elif key == "SIG_CREATED": @@ -1277,7 +1277,8 @@ class Verify(object): 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", "PROGRESS"): + "DECRYPTION_OKAY", "INV_SGNR", "PROGRESS", + "PINENTRY_LAUNCHED"): pass elif key == "BADSIG": self.valid = False From 2c57c0f6d0460d5a5d46a91869533b6b3c4db0c5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 11 Mar 2015 03:25:55 +0000 Subject: [PATCH 12/26] Handle [GOOD|BAD|MISSING]_PASSPHRASE statuses in _parsers.ListPackets. * FIXES #100. --- gnupg/_parsers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index d305842..9de57d2 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -1531,21 +1531,21 @@ class ListPackets(object): :raises: :exc:`~exceptions.ValueError` if the status message is unknown. """ - if key == 'NODATA': + if key in ('NO_SECKEY', 'BEGIN_DECRYPTION', 'DECRYPTION_FAILED', + 'END_DECRYPTION', 'GOOD_PASSPHRASE', 'BAD_PASSPHRASE'): + pass + elif key == 'NODATA': self.status = nodata(value) elif key == 'ENC_TO': key, _, _ = value.split() if not self.key: self.key = key self.encrypted_to.append(key) - elif key == 'NEED_PASSPHRASE': + elif key == ('NEED_PASSPHRASE', 'MISSING_PASSPHRASE'): self.need_passphrase = True elif key == 'NEED_PASSPHRASE_SYM': self.need_passphrase_sym = True elif key == 'USERID_HINT': self.userid_hint = value.strip().split() - elif key in ('NO_SECKEY', 'BEGIN_DECRYPTION', 'DECRYPTION_FAILED', - 'END_DECRYPTION'): - pass else: raise ValueError("Unknown status message: %r" % key) From 2cf3dd1c86daf88871e218f3cc21a2060266ccd7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 02:20:13 +0000 Subject: [PATCH 13/26] Add coverage related commands to Makefile and clean up test directives. --- Makefile | 71 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index c992d03..52cef67 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ SHELL=/bin/sh TESTDIR=./gnupg/test TESTHANDLE=$(TESTDIR)/test_gnupg.py FILES=$(SHELL find ./gnupg/ -name "*.py" -printf "%p,") +PYTHON=$(SHELL which python) +PYTHON3=$(SHELL which python3) PKG_NAME=python-gnupg DOC_DIR=docs DOC_BUILD_DIR:=$(DOC_DIR)/_build @@ -50,23 +52,70 @@ test-before: cleanup-src cleanup-tests which python && python --version -which pip && pip --version && pip list -test: test-before - python $(TESTHANDLE) basic encodings parsers keyrings listkeys genkey \ - sign crypt +test-run: test-before + python $(TESTHANDLE) \ + basic \ + encodings \ + parsers \ + keyrings \ + listkeys \ + genkey \ + sign \ + crypt + +py3k-test-run: test-before + python3 $(TESTHANDLE) \ + basic \ + encodings \ + parsers \ + keyrings \ + listkeys \ + genkey \ + sign \ + crypt + +coverage-run: test-before + coverage run --rcfile=".coveragerc" $(PYTHON) $(TESTHANDLE) \ + basic \ + encodings \ + parsers \ + keyrings \ + listkeys \ + genkeys \ + sign \ + crypt + +py3k-coverage-run: test-before + coverage run --rcfile=".coveragerc" $(PYTHON3) $(TESTHANDLE) \ + basic \ + encodings \ + parsers \ + keyrings \ + listkeys \ + genkeys \ + sign \ + crypt + +coverage-report: + coverage report --rcfile=".coveragerc" + +coverage-html: + coverage html --rcfile=".coveragerc" + +clean-test: 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* -py3k-test: test-before - python3 $(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* +test: test-run clean-test + +py3k-test: py3k-test-run clean-test + +coverage: coverage-run coverage-report coverage-html clean-test + +py3k-coverage: py3k-coverage-run coverage-report coverage-html clean-test install: python setup.py install --record installed-files.txt From 749ef6fa00e9f67affa1426e4081f6f512b36de9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 02:20:48 +0000 Subject: [PATCH 14/26] PEP8 whitespace fixes in gnupg/_meta.py. --- gnupg/_meta.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index f93bae4..68d2873 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -994,16 +994,13 @@ class GPGBase(object): for recp in recipients.split(' '): self._add_recipient_string(args, hidden_recipients, 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("Got data '%s' with type '%s'." % (data, type(data))) + self._handle_io(args, data, result, passphrase=passphrase, binary=True) log.debug("\n%s" % result.data) if output_filename: From 657be31ae1cc86edf3cab42bbd552b7b2c3a5618 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 02:21:18 +0000 Subject: [PATCH 15/26] Change a str to a repr in a log message. --- gnupg/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 68d2873..7804e03 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -995,7 +995,7 @@ class GPGBase(object): self._add_recipient_string(args, hidden_recipients, recp) ## ...and now that we've proven py3k is better... else: - log.debug("Don't know what to do with recipients: '%s'" + log.debug("Don't know what to do with recipients: %r" % recipients) result = self._result_map['crypt'](self) From af403fe14446294e6c11482566af50fd53d49ce7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 02:25:10 +0000 Subject: [PATCH 16/26] Move 'test_recv_keys_default' to a new test group which doesn't run. * FIXES #99 temporarily. --- gnupg/test/test_gnupg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index fae4d21..1fee9ca 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -1203,8 +1203,9 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_secret_keyring', 'test_import_and_export', 'test_deletion', - 'test_import_only', - 'test_recv_keys_default',]), } + 'test_import_only']), + 'recvkeys': set(['test_recv_keys_default']), +} def main(args): if not args.quiet: From 782a81b46a4f7d36cff1cb81be27a63574463a77 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 10 Mar 2015 01:06:20 +0000 Subject: [PATCH 17/26] Split encryption tests for file-like objects into multiple tests. This modifies the tests added in #89. --- gnupg/test/test_gnupg.py | 74 ++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 1fee9ca..b3aef22 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -777,14 +777,12 @@ authentication.""" log.debug("Encrypted: %s" % encrypted) self.assertNotEquals(message, encrypted) - def test_encryption_of_file_like_objects(self): - """Test encryption of file-like objects""" - key = self.generate_key("Craig Gentry", "xorr.ox", - passphrase="craiggentry") - gentry_fpr = str(key.fingerprint) + def _encryption_test_setup(self): + passphrase = "craiggentry" + key = self.generate_key("Craig Gentry", "xorr.ox", passphrase=passphrase) + fpr = str(key.fingerprint) gentry = self.gpg.export_keys(key.fingerprint) self.gpg.import_keys(gentry) - message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic encryption to a hybrid wireless sensor/mesh network. The system enables @@ -792,32 +790,54 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" + return (message, fpr, passphrase) - def _encryption_test_wrapper(stream_type, message): - stream = stream_type(message) - encrypted = str(self.gpg.encrypt(stream, gentry_fpr)) - decrypted = str(self.gpg.decrypt(encrypted, - passphrase="craiggentry")) - self.assertEqual(message, decrypted) + def _encryption_test(self, stream_type, message, fingerprint, passphrase): + stream = stream_type(message) + encrypted = str(self.gpg.encrypt(stream, fingerprint)) + decrypted = str(self.gpg.decrypt(encrypted, passphrase=passphrase)) + self.assertEqual(message, decrypted) + + def test_encryption_of_file_like_objects_io_StringIO(self): + """Test encryption of file-like object io.StringIO.""" + message, fpr, passphrase = self._encryption_test_setup() - # Test io.StringIO and io.BytesIO (Python 2.6+) try: - from io import StringIO, BytesIO - _encryption_test_wrapper(StringIO, unicode(message)) - _encryption_test_wrapper(BytesIO, message) + from io import StringIO + self._encryption_test(StringIO, message, fpr, passphrase) except ImportError: pass - # Test StringIO.StringIO - from StringIO import StringIO - _encryption_test_wrapper(StringIO, message) + def test_encryption_of_file_like_objects_io_BytesIO(self): + """Test encryption of file-like object io.BytesIO.""" + message, fpr, passphrase = self._encryption_test_setup() - # Test cStringIO.StringIO - from cStringIO import StringIO - _encryption_test_wrapper(StringIO, message) + try: + from io import BytesIO + if _util._py3k: + self._encryption_test(BytesIO, bytes(message, 'utf-8'), fpr, passphrase) + else: + self._encryption_test(BytesIO, str(message), fpr, passphrase) + except ImportError: + pass + + def test_encryption_of_file_like_objects_StringIO_StringIO(self): + """Test encryption of file-like object StringIO.StringIO (Python2 only).""" + message, fpr, passphrase = self._encryption_test_setup() + + if not _util._py3k: + from StringIO import StringIO + self._encryption_test(StringIO, message, fpr, passphrase) + + def test_encryption_of_file_like_objects_cStringIO_StringIO(self): + """Test encryption of file-like object cStringIO.StringIO (Python2 only).""" + message, fpr, passphrase = self._encryption_test_setup() + + if not _util._py3k: + from cStringIO import StringIO + self._encryption_test(StringIO, message, fpr, passphrase) def test_encryption_alt_encoding(self): - """Test encryption with latin-1 encoding""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") @@ -998,7 +1018,8 @@ boolean circuit causes a considerable overhead.""" ## We expect Alice's key to be hidden (returned as zero's) and Bob's ## key to be there. expected_values = ["0000000000000000", "0000000000000000"] - self.assertEquals(expected_values, self.gpg.list_packets(encrypted).encrypted_to) + packets = self.gpg.list_packets(encrypted) + self.assertEquals(expected_values, packets.encrypted_to) def test_encryption_decryption_multi_recipient(self): """Test decryption of an encrypted string for multiple users""" @@ -1187,7 +1208,10 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_string_verification', 'test_signature_string_algorithm_encoding']), 'crypt': set(['test_encryption', - 'test_encryption_of_file_like_objects', + 'test_encryption_of_file_like_objects_io_StringIO', + 'test_encryption_of_file_like_objects_io_BytesIO', + 'test_encryption_of_file_like_objects_StringIO_StringIO', + 'test_encryption_of_file_like_objects_cStringIO_StringIO', 'test_encryption_alt_encoding', 'test_encryption_multi_recipient', 'test_encryption_decryption_multi_recipient', From b97091770109615df6dc77e070b07620e46178e4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 10 Mar 2015 01:24:26 +0000 Subject: [PATCH 18/26] Fix multiple encoding errors in tests. --- gnupg/test/test_gnupg.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index b3aef22..ecd1e58 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -26,6 +26,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 @@ -389,7 +390,10 @@ class GPGTestCase(unittest.TestCase): def test_gen_key_input(self): """Test that GnuPG batch file creation is successful.""" key_input = self.generate_key_input("Francisco Ferrer", "an.ok") - self.assertIsInstance(key_input, str) + if _util._py3k: + self.assertIsInstance(key_input, str) + else: + self.assertIsInstance(key_input, basestring) self.assertGreater(key_input.find('Francisco Ferrer'), 0) def test_rsa_key_generation(self): @@ -794,8 +798,14 @@ authentication.""" def _encryption_test(self, stream_type, message, fingerprint, passphrase): stream = stream_type(message) - encrypted = str(self.gpg.encrypt(stream, fingerprint)) - decrypted = str(self.gpg.decrypt(encrypted, passphrase=passphrase)) + encrypted = self.gpg.encrypt(stream, fingerprint).data + decrypted = self.gpg.decrypt(encrypted, passphrase=passphrase).data + + if isinstance(decrypted, bytes): + decrypted = decrypted.decode() + if isinstance(message, bytes): + message = message.decode() + self.assertEqual(message, decrypted) def test_encryption_of_file_like_objects_io_StringIO(self): @@ -804,7 +814,10 @@ authentication.""" try: from io import StringIO - self._encryption_test(StringIO, message, fpr, passphrase) + if _util._py3k: + self._encryption_test(StringIO, message, fpr, passphrase) + else: + self._encryption_test(StringIO, unicode(message), fpr, passphrase) except ImportError: pass @@ -817,7 +830,7 @@ authentication.""" if _util._py3k: self._encryption_test(BytesIO, bytes(message, 'utf-8'), fpr, passphrase) else: - self._encryption_test(BytesIO, str(message), fpr, passphrase) + self._encryption_test(BytesIO, message, fpr, passphrase) except ImportError: pass @@ -845,11 +858,7 @@ authentication.""" key = self.generate_key("Marten van Dijk", "xorr.ox") dijk = str(key.fingerprint) 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 = u'Hello, André!'.encode(self.gpg._encoding) encrypted = self.gpg.encrypt(data, gentry) edata = str(encrypted.data) self.assertNotEqual(data, edata) From 4be6fb75e387039c51224863b67729535ba6888a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 10 Mar 2015 01:26:16 +0000 Subject: [PATCH 19/26] Fix potential UnicodeEncodeError in gen_key_input(). --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 12b46ce..ae15293 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -801,7 +801,7 @@ class GPG(GPGBase): key = key.replace('_','-').title() ## to set 'cert', 'Key-Usage' must be blank string if not key in ('Key-Usage', 'Subkey-Usage'): - if str(val).strip(): + if type(u'')(val).strip(): parms[key] = val ## if Key-Type is 'default', make Subkey-Type also be 'default' From a7e772f10a951ee81e9c467cad5aee8465dcd5bb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 11 Mar 2015 04:01:28 +0000 Subject: [PATCH 20/26] Make an open() file mode explicitly binary. It already was binary, due to the `from codecs import open as open`, however we should be more explicit. --- gnupg/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 7804e03..ed0babf 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -1005,7 +1005,7 @@ class GPGBase(object): if output_filename: log.info("Writing encrypted output to file: %s" % output_filename) - with open(output_filename, 'w+') as fh: + with open(output_filename, 'wb') as fh: fh.write(result.data) fh.flush() log.info("Encrypted output written successfully.") From 43164fa7dbf775c5cf426c6bdc206ab11df0f8bc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 02:09:42 +0000 Subject: [PATCH 21/26] Fix Python3 stream encoding issues in _copy_data(). These issues were introduced in f8ccdc50. Because we no longer convert everything to an io.BytesIO in _encrypt() with _make_binary_stream(), all io.StringIO()s which are passed through must take encoded strings and io.BytesIO()s must take bytes (and there is actually a difference with Python3). Additionally, there appears to be an issue where the `outstream` passed to _copy_data() is sometimes a _io.BufferedWriter and other times an encodings.utf_8.StreamWriter. I am not sure yet where this problem was introduced. For now, the workaround for dealing with the Python3 bytes/str io.BytesIO/io.StringIO problem also provides a workaround for this issue. * FIXES #88. * FIXES #89 for Python3. * FIXES #93. --- gnupg/_util.py | 78 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index 64ee6eb..c9c288c 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -167,7 +167,6 @@ def _copy_data(instream, outstream): :param file outstream: The file descriptor of a tmpfile to write to. """ sent = 0 - coder = find_encodings() while True: @@ -179,24 +178,73 @@ def _copy_data(instream, outstream): data = instream.read(1024) if len(data) == 0: break + sent += len(data) - log.debug("Sending chunk %d bytes:\n%s" - % (sent, data)) - try: - outstream.write(data) - except UnicodeError: + log.debug("Sending chunk %d bytes:\n%s" % (sent, data)) + + if _py3k and isinstance(data, bytes): + encoded = coder.encode(data.decode(coder.name))[0] + elif _py3k and isinstance(data, str): + encoded = coder.encode(data)[0] + elif not _py3k and type(data) is not str: + encoded = coder.encode(data)[0] + else: + encoded = data + log.debug("Writing encoded data with type %s to outstream... " + % type(encoded)) + + if not _py3k: try: - outstream.write(coder.encode(data)) - except IOError: - log.exception("Error sending data: Broken pipe") + outstream.write(encoded) + except IOError as ioe: + # Can get 'broken pipe' errors even when all data was sent + if 'Broken pipe' in str(ioe): + log.error('Error sending data: Broken pipe') + else: + log.exception(ioe) break - except IOError as ioe: - # Can get 'broken pipe' errors even when all data was sent - if 'Broken pipe' in str(ioe): - log.error('Error sending data: Broken pipe') else: - log.exception(ioe) - break + log.debug("Wrote data type to outstream.") + else: + try: + outstream.write(bytes(encoded)) + except TypeError as te: + # XXX FIXME This appears to happen because + # _threaded_copy_data() sometimes passes the `outstream` as an + # object with type <_io.BufferredWriter> and at other times + # with type . We hit the + # following error when the `outstream` has type + # . + if not "convert 'bytes' object to str implicitly" in str(te): + log.error(str(te)) + try: + outstream.write(encoded.decode()) + except TypeError as yate: + # We hit the "'str' does not support the buffer interface" + # error in Python3 when the `outstream` is an io.BytesIO and + # we try to write a str to it. We don't care about that + # error, we'll just try again with bytes. + if not "does not support the buffer interface" in str(yate): + log.error(str(yate)) + except IOError as ioe: + # Can get 'broken pipe' errors even when all data was sent + if 'Broken pipe' in str(ioe): + log.error('Error sending data: Broken pipe') + else: + log.exception(ioe) + break + else: + log.debug("Wrote data type outstream.") + except IOError as ioe: + # Can get 'broken pipe' errors even when all data was sent + if 'Broken pipe' in str(ioe): + log.error('Error sending data: Broken pipe') + else: + log.exception(ioe) + break + else: + log.debug("Wrote data type to outstream.") + try: outstream.close() except IOError as ioe: From 38685ae0019a072da1ac178c5b9c95841cd710ba Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 03:18:52 +0000 Subject: [PATCH 22/26] Add @doktorstick's example code for reproducing Issue #93 as a test. --- gnupg/test/test_gnupg.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index ecd1e58..86491cc 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -1178,6 +1178,20 @@ know, maybe you shouldn't be doing it in the first place. encrypted_message = fh.read() log.debug("Encrypted file contains:\n\n%s\n" % encrypted_message) + def test_encryption_from_filehandle(self): + """Test that ``encrypt(open('foo'), ...)`` is successful.""" + message_filename = os.path.join(_files, 'cypherpunk_manifesto') + with open(message_filename, 'rb') as f: + kwargs = dict(passphrase='speedtest', + symmetric=True, + cipher_algo='AES256', + armor=False, + encrypt=False, + output='/tmp/purernd.enc.gnupg') + encrypted = self.gpg.encrypt(f, None, **kwargs) + self.assertTrue(encrypted.ok) + self.assertGreater(len(encrypted.data), 0) + suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_fix_unsafe_semicolon', @@ -1230,7 +1244,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_symmetric_encryption_and_decryption', 'test_file_encryption_and_decryption', 'test_encryption_to_filename', - 'test_encryption_to_filehandle',]), + 'test_encryption_to_filehandle', + 'test_encryption_from_filehandle',]), 'listkeys': set(['test_list_keys_after_generation']), 'keyrings': set(['test_public_keyring', 'test_secret_keyring', From 0c87da3d78080d7d07878e7e1421fe19eebccf73 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 03:20:32 +0000 Subject: [PATCH 23/26] Recognise builtin Python2 and Python3 file handle types as streams. * FIXES Issue #93 for both Python2 and Python3. --- gnupg/_util.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/gnupg/_util.py b/gnupg/_util.py index c9c288c..9a7bc1b 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -40,13 +40,15 @@ _STREAMLIKE_TYPES = [] # These StringIO classes are actually utilised. try: + import io from io import StringIO from io import BytesIO except ImportError: from cStringIO import StringIO else: - _STREAMLIKE_TYPES.append(BytesIO) - _STREAMLIKE_TYPES.append(StringIO) + # The io.IOBase type covers the above example for an open file handle in + # Python3, as well as both io.BytesIO and io.StringIO. + _STREAMLIKE_TYPES.append(io.IOBase) # The remaining StringIO classes which are imported are used to determine if a # object is a stream-like in :func:`_is_stream`. @@ -65,6 +67,20 @@ if sys.version_info.major == 2: _STREAMLIKE_TYPES.append(_cStringIO.InputType) _STREAMLIKE_TYPES.append(_cStringIO.OutputType) + # In Python2: + # + # >>> type(open('README.md', 'rb')) + # + # + # whereas, in Python3, the `file` builtin doesn't exist and instead we get: + # + # >>> type(open('README.md', 'rb')) + # <_io.BufferedReader name='README.md'> + # + # which is covered by the above addition of io.IOBase. + _STREAMLIKE_TYPES.append(file) + + from . import _logger From 6d1890389c7b4a8a6ab252c51bc9f95568ec4d84 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 03:44:12 +0000 Subject: [PATCH 24/26] Actually check output file contents in to test_encrypt_*() tests. This provides more accurate testing for issues like #93. --- gnupg/test/test_gnupg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 86491cc..d6bbb6e 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -1152,9 +1152,9 @@ know, maybe you shouldn't be doing it in the first place. self.assertTrue(os.path.isfile(output)) # Check the contents: - with open(output) as fh: + with open(output, 'rb') as fh: encrypted_message = fh.read() - log.debug("Encrypted file contains:\n\n%s\n" % encrypted_message) + self.assertTrue(b"-----BEGIN PGP MESSAGE-----" in encrypted_message) def test_encryption_to_filehandle(self): """Test that ``encrypt(..., output=filelikething)`` is successful.""" @@ -1174,9 +1174,9 @@ know, maybe you shouldn't be doing it in the first place. self.assertTrue(os.path.isfile(output)) # Check the contents: - with open(output) as fh: + with open(output, 'rb') as fh: encrypted_message = fh.read() - log.debug("Encrypted file contains:\n\n%s\n" % encrypted_message) + self.assertTrue(b"-----BEGIN PGP MESSAGE-----" in encrypted_message) def test_encryption_from_filehandle(self): """Test that ``encrypt(open('foo'), ...)`` is successful.""" From 79285c4c17bc915dc0340d8ba5444a453c9dd861 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 03:45:20 +0000 Subject: [PATCH 25/26] Add @doktorstick's example code for reproducing #93 as a unittest. * ADD regression test for #93. --- gnupg/test/test_gnupg.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index d6bbb6e..484fdc2 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -1192,6 +1192,28 @@ know, maybe you shouldn't be doing it in the first place. self.assertTrue(encrypted.ok) self.assertGreater(len(encrypted.data), 0) + def test_encryption_with_output(self): + """Test that ``encrypt('foo', ..., output='/foo/bar/baz')`` is successful.""" + message_filename = os.path.join(_files, 'cypherpunk_manifesto') + with open (message_filename, 'rb') as f: + data = f.read() + + output = os.path.join(self.gpg.homedir, 'test-encryption-with-output.gpg') + kwargs = dict(passphrase='speedtest', + symmetric=True, + cipher_algo='AES256', + encrypt=False, + output=output) + encrypted = self.gpg.encrypt(data, None, **kwargs) + self.assertTrue(encrypted.ok) + self.assertGreater(len(encrypted.data), 0) + self.assertTrue(os.path.isfile(output)) + + # Check the contents: + with open(output, 'rb') as fh: + encrypted_message = fh.read() + self.assertTrue(b"-----BEGIN PGP MESSAGE-----" in encrypted_message) + suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_fix_unsafe_semicolon', @@ -1245,7 +1267,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_file_encryption_and_decryption', 'test_encryption_to_filename', 'test_encryption_to_filehandle', - 'test_encryption_from_filehandle',]), + 'test_encryption_from_filehandle', + 'test_encryption_with_output',]), 'listkeys': set(['test_list_keys_after_generation']), 'keyrings': set(['test_public_keyring', 'test_secret_keyring', From b7ff69092a145affc9c36886b0b6e7216e517a81 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 13 Mar 2015 03:47:42 +0000 Subject: [PATCH 26/26] Avoid writing to /tmp and borking terminals in regression test from #94. --- 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 86491cc..f5dabd2 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -1182,12 +1182,12 @@ know, maybe you shouldn't be doing it in the first place. """Test that ``encrypt(open('foo'), ...)`` is successful.""" message_filename = os.path.join(_files, 'cypherpunk_manifesto') with open(message_filename, 'rb') as f: + output = os.path.join(self.gpg.homedir, 'test-encryption-from-filehandle.gpg') kwargs = dict(passphrase='speedtest', symmetric=True, cipher_algo='AES256', - armor=False, encrypt=False, - output='/tmp/purernd.enc.gnupg') + output=output) encrypted = self.gpg.encrypt(f, None, **kwargs) self.assertTrue(encrypted.ok) self.assertGreater(len(encrypted.data), 0)