diff --git a/gnupg/_meta.py b/gnupg/_meta.py index f3017cf..32ab287 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -46,6 +46,8 @@ except ImportError: from . import _parsers from . import _util +from ._util import b +from ._util import s from ._parsers import _check_preferences from ._parsers import _sanitise_list @@ -804,6 +806,19 @@ class GPGBase(object): ## We could use _handle_io here except for the fact that if the ## passphrase is bad, gpg bails and you can't write the message. result = self._result_map['sign'](self) + + ## If the passphrase is an empty string, the message up to and + ## including its first newline will be cut off before making it to the + ## GnuPG process. Therefore, if the passphrase='' or passphrase=b'', + ## we set passphrase=None. See Issue #82: + ## https://github.com/isislovecruft/python-gnupg/issues/82 + if _util._is_string(passphrase): + passphrase = passphrase if len(passphrase) > 0 else None + elif _util._is_bytes(passphrase): + passphrase = s(passphrase) if len(passphrase) > 0 else None + else: + passphrase = None + proc = self._open_subprocess(args, passphrase is not None) try: if passphrase: diff --git a/gnupg/_util.py b/gnupg/_util.py index ba37a7a..d25aa92 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -165,6 +165,33 @@ def find_encodings(enc=None, system=False): return coder + +if _py3k: + def b(x): + """See http://python3porting.com/problems.html#nicer-solutions""" + return x + + def s(x): + if isinstance(x, str): + return x + elif isinstance(x, (bytes, bytearray)): + return x.decode(find_encodings().name) + else: + raise NotImplemented +else: + def b(x): + """See http://python3porting.com/problems.html#nicer-solutions""" + return find_encodings().encode(x)[0] + + def s(x): + if isinstance(x, basestring): + return x + elif isinstance(x, (bytes, bytearray)): + return x.decode(find_encodings().name) + else: + raise NotImplemented + + def author_info(name, contact=None, public_key=None): """Easy object-oriented representation of contributor info. @@ -440,6 +467,31 @@ def _is_stream(input): """ return isinstance(input, tuple(_STREAMLIKE_TYPES)) +def _is_string(thing): + """Check that **thing** is a string. The definition of the latter depends + upon the Python version. + + :param thing: The thing to check if it's a string. + :rtype: bool + :returns: ``True`` if **thing** is string (or unicode in Python2). + """ + if (_py3k and isinstance(thing, str)): + return True + if (not _py3k and isinstance(thing, basestring)): + return True + return False + +def _is_bytes(thing): + """Check that **thing** is bytes. + + :param thing: The thing to check if it's bytes. + :rtype: bool + :returns: ``True`` if **thing** is bytes or a bytearray. + """ + if isinstance(thing, (bytes, bytearray)): + return True + return False + def _is_list_or_tuple(instance): """Check that ``instance`` is a list or tuple. diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index e8be11a..da99ca2 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -626,6 +626,66 @@ class GPGTestCase(unittest.TestCase): passphrase='wrong horse battery staple') self.assertFalse(sig, "Bad passphrase should fail") + def test_signature_string_passphrase_empty_string(self): + """Test that a signing attempt with passphrase='' creates a valid + signature. + + See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82 + """ + with open(os.path.join(_files, 'test_key_1.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase='') + self.assertTrue(sig) + self.assertTrue(message in str(sig)) + + def test_signature_string_passphrase_empty_bytes_literal(self): + """Test that a signing attempt with passphrase=b'' creates a valid + signature. + + See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82 + """ + with open(os.path.join(_files, 'test_key_1.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase=b'') + self.assertTrue(sig) + print("%r" % str(sig)) + self.assertTrue(message in str(sig)) + + def test_signature_string_passphrase_bytes_literal(self): + """Test that a signing attempt with passphrase=b'overalls' creates a + valid signature. + """ + with open(os.path.join(_files, 'kat.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase=b'overalls') + self.assertTrue(sig) + print("%r" % str(sig)) + self.assertTrue(message in str(sig)) + + def test_signature_string_passphrase_None(self): + """Test that a signing attempt with passphrase=None fails creates a + valid signature. + + See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82 + """ + with open(os.path.join(_files, 'test_key_1.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase=None) + self.assertTrue(sig) + self.assertTrue(message in str(sig)) + def test_signature_file(self): """Test that signing a message file works.""" key = self.generate_key("Leonard Adleman", "rsa.com") @@ -1339,6 +1399,10 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_verification_detached', 'test_signature_verification_detached_binary', 'test_signature_file', + 'test_signature_string_passphrase_empty_string', + 'test_signature_string_passphrase_empty_bytes_literal', + 'test_signature_string_passphrase_bytes_literal', + 'test_signature_string_passphrase_None', 'test_signature_string_bad_passphrase', 'test_signature_string_verification', 'test_signature_string_algorithm_encoding']),