From e4f2d533b1a313a2693d0d11405235ec4a82dc24 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 17:48:04 +0000 Subject: [PATCH] Add cipher, compress, and digest preferences and rename parameters. * Rename gpghome to homedir. * Rename gpgbinary to binary. * Add setting to append '--no-use-agent' to the command string in an attempt to overcome bugs resulting on systems where the user has gpg-agent running in the background (with some configurations, this is run before X is started, and killing the agent can result in X dying) and GnuPG tries to call the program specified by the symlink at /usr/bin/pinentry, result in encryption and decryption failing due to the '--batch' option blasting through pinentry without input. This results in a complaint from GnuPG: gpg: Sorry, no terminal at all requested - can't get input This bug also prevents symmetric encryption/decryption from working in a similar manner. Daniel Kahn Gilmor's monkeysphere, if I am recalling correctly, has a hack to remove the $DISPLAY variable from the users environment, and then add it back in, but frankly this option frightens me, as unsetting the display could result in all X applications failing. Werner Koch's suggestions, from the gnupg-devel mailing list are: http://lists.gnupg.org/pipermail/gnupg-users/2007-April/030927.html And, for the record, '--no-use-agent' doesn't disable pinentry. --- gnupg/gnupg.py | 87 +++++++++++++++++++++------------------ gnupg/tests/test_gnupg.py | 31 +++++++------- gnupg/util.py | 37 +++++++++-------- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 6e50d18..9716e61 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -122,27 +122,27 @@ class GPG(object): 'sign': Sign, 'verify': Verify,} - def __init__(self, gpgbinary=None, gpghome=None, verbose=False, - use_agent=False, keyring=None, secring=None, pubring=None, - options=None): + def __init__(self, binary=None, homedir=None, verbose=False, + use_agent=False, keyring=None, secring=None, + default_preference_list=None, options=None): """Initialize a GnuPG process wrapper. - :param str gpgbinary: Name for GnuPG binary executable. If the absolute + :param str binary: Name for GnuPG binary executable. If the absolute path is not given, the evironment variable $PATH is searched for the executable and checked that the real uid/gid of the user has sufficient permissions. - :param str gpghome: Full pathname to directory containing the public + :param str homedir: Full pathname to directory containing the public and private keyrings. Default is whatever GnuPG defaults to. :param str keyring: raises :exc:DeprecationWarning. Use :param:pubring. :param str secring: Name of alternative secret keyring file to use. If left unspecified, this will default to using - 'secring.gpg' in the :param:gpghome directory, and + 'secring.gpg' in the :param:homedir directory, and create that file if it does not exist. :param str pubring: Name of alternative public keyring file to use. If left unspecified, this will default to using - 'pubring.gpg' in the :param:gpghome directory, and + 'pubring.gpg' in the :param:homedir directory, and create that file if it does not exist. :param list options: A list of additional options to pass to the GPG binary. @@ -150,24 +150,28 @@ class GPG(object): problem invoking gpg. """ - if not gpghome: - gpghome = _conf - self.gpghome = _fix_unsafe(gpghome) - if self.gpghome: - _util._create_gpghome(self.gpghome) + if not homedir: + homedir = _conf + self.homedir = _fix_unsafe(homedir) + if self.homedir: + _util._create_homedir(self.homedir) else: - message = ("Unsuitable gpg home dir: %s" % gpghome) + message = ("Unsuitable gpg home dir: %s" % homedir) logger.debug("GPG.__init__(): %s" % message) - self.gpgbinary = _util._find_gpgbinary(gpgbinary) + self.binary = _util._find_binary(binary) - if keyring is not None: - raise DeprecationWarning("Option 'keyring' changing to 'secring'") + if default_preference_list is None: + prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' + else: + ## xxx implement me, should return None on error + prefs = check_preference_list(default_preference_list) + self.default_preference_list = prefs secring = 'secring.gpg' if secring is None else _fix_unsafe(secring) - pubring = 'pubring.gpg' if pubring is None else _fix_unsafe(pubring) - self.secring = os.path.join(self.gpghome, secring) - self.pubring = os.path.join(self.gpghome, pubring) + keyring = 'pubring.gpg' if keyring is None else _fix_unsafe(keyring) + self.secring = os.path.join(self.homedir, secring) + self.keyring = os.path.join(self.homedir, keyring) self.options = _sanitise(options) if options else None @@ -176,10 +180,10 @@ class GPG(object): self.encoding = sys.stdin.encoding try: - assert self.gpghome is not None, "Got None for self.gpghome" - assert _util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" - % self.gpghome) - assert self.gpgbinary, "Could not find gpgbinary %s" % full + assert self.homedir is not None, "Got None for self.homedir" + assert _util._has_readwrite(self.homedir), ("Home dir %s needs r+w" + % self.homedir) + assert self.binary, "Could not find binary %s" % full assert isinstance(verbose, bool), "'verbose' must be boolean" assert isinstance(use_agent, bool), "'use_agent' must be boolean" if self.options: @@ -205,17 +209,19 @@ class GPG(object): :func:parsers._sanitise. 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 --no-emit-version'] - if self.gpghome: - cmd.append('--homedir "%s"' % self.gpghome) - if self.pubring: - cmd.append('--no-default-keyring --keyring %s' % self.pubring) + cmd = [self.binary, '--status-fd 2 --no-tty --no-emit-version'] + if self.homedir: + cmd.append('--homedir "%s"' % self.homedir) + if self.keyring: + cmd.append('--no-default-keyring --keyring %s' % self.keyring) if self.secring: cmd.append('--secret-keyring %s' % self.secring) if passphrase: cmd.append('--batch --passphrase-fd 0') if self.use_agent: cmd.append('--use-agent') + else: + cmd.append('--no-use-agent') if self.options: [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] if args: @@ -375,7 +381,7 @@ class GPG(object): def verify(self, data): """Verify the signature on the contents of the string ``data``. - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input(Passphrase='foo') >>> key = gpg.gen_key(input) >>> assert key @@ -440,7 +446,7 @@ class GPG(object): >>> import shutil >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -495,7 +501,7 @@ class GPG(object): >>> import shutil >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> result = gpg.recv_keys('pgp.mit.edu', '3FF0DB166A7476EA') >>> assert result @@ -583,7 +589,7 @@ class GPG(object): >>> import shutil >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -639,7 +645,7 @@ class GPG(object): """Generate a GnuPG key through batch file key generation. See :meth:`GPG.gen_key_input()` for creating the control input. - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> assert result @@ -687,7 +693,7 @@ class GPG(object): %secring foo.sec %commit - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> params = {'name_real':'python-gnupg tester', 'name_email':'test@ing'} >>> key_input = gpg.gen_key_input(**params) >>> result = gpg.gen_key(input) @@ -850,7 +856,7 @@ class GPG(object): for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) - out += "%%pubring %s\n" % self.pubring + out += "%%pubring %s\n" % self.keyring out += "%%secring %s\n" % self.secring if testing: @@ -922,7 +928,7 @@ class GPG(object): >>> import shutil >>> if os.path.exists("keys"): ... shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input(passphrase='foo') >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -1001,10 +1007,10 @@ class GPGWrapper(GPG): replaced by a more general class used throughout the project. """ - def __init__(self, gpgbinary=None, gnupghome=_conf, + def __init__(self, binary=None, homedir=_conf, verbose=False, use_agent=False, keyring=None, options=None): super(GPGWrapper, self).__init__(gnupghome=gnupghome, - gpgbinary=gpgbinary, + binary=binary, verbose=verbose, use_agent=use_agent, keyring=keyring, @@ -1036,13 +1042,14 @@ class GPGWrapper(GPG): raise LookupError( "GnuPG public key for subkey %s not found!" % subkey) - def encrypt(self, data, recipient, sign=None, always_trust=True, + def encrypt(self, data, recipient, sign_with=None, always_trust=True, passphrase=None, symmetric=False): """ Encrypt data using GPG. """ # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).encrypt(data, recipient, sign=sign, + return super(GPGWrapper, self).encrypt(data, recipient, + sign_with=sign, always_trust=always_trust, passphrase=passphrase, symmetric=symmetric, diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 35a2d11..c3c1c89 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -142,8 +142,8 @@ class GPGTestCase(unittest.TestCase): self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) shutil.rmtree(hd) self.homedir = hd - self.gpg = gnupg.GPG(gpghome=hd, gpgbinary='gpg') - self.pubring = os.path.join(self.homedir, 'pubring.gpg') + self.gpg = gnupg.GPG(homedir=hd, binary='gpg') + self.keyring = os.path.join(self.homedir, 'keyring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') def test_parsers_fix_unsafe(self): @@ -198,24 +198,24 @@ class GPGTestCase(unittest.TestCase): env_copy = os.environ path_copy = os.environ.pop('PATH') with self.assertRaises(RuntimeError): - gnupg.GPG(gpghome=self.homedir) + gnupg.GPG(homedir=self.homedir) os.environ = env_copy os.environ.update({'PATH': path_copy}) def test_gpg_binary_not_abs(self): """Test that a non-absolute path to gpg results in a full path.""" - self.assertTrue(os.path.isabs(self.gpg.gpgbinary)) + 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'] - self.gpg.keyring = self.secring cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty --no-emit-version', - '--homedir "%s"' % HOME_DIR, - '--no-default-keyring --keyring %s' % self.pubring, - '--secret-keyring %s' % self.secring] + '--homedir "%s"' % self.homedir, + '--no-default-keyring --keyring %s' % self.keyring, + '--secret-keyring %s' % self.secring, + '--no-use-agent',] self.assertListEqual(cmd, expected) def test_make_args(self): @@ -223,7 +223,7 @@ class GPGTestCase(unittest.TestCase): not_allowed = ['--bicycle', '--zeppelin', 'train', 'flying-carpet'] self.gpg.options = not_allowed[:-2] args = self.gpg._make_args(not_allowed[2:], False) - self.assertTrue(len(args) == 5) + self.assertTrue(len(args) == 6) for na in not_allowed: self.assertNotIn(na, args) @@ -410,13 +410,15 @@ class GPGTestCase(unittest.TestCase): def test_public_keyring(self): """Test that the public keyring is found in the gpg home directory.""" - self.gpg.keyring = self.pubring - self.assertTrue(os.path.isfile(self.pubring)) + ## we have to use the keyring for GnuPG to create it: + keys = self.gpg.list_keys() + self.assertTrue(os.path.isfile(self.gpg.keyring)) def test_secret_keyring(self): """Test that the secret keyring is found in the gpg home directory.""" - self.gpg.keyring = self.secring - self.assertTrue(os.path.isfile(self.secring)) + ## we have to use the secring for GnuPG to create it: + keys = self.gpg.list_keys(secret=True) + self.assertTrue(os.path.isfile(self.gpg.secring)) def test_import_and_export(self): """Test that key import and export works.""" @@ -725,7 +727,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_valid', 'test_parsers_is_hex_invalid', 'test_copy_data_bytesio',]), - 'basic': set(['test_gpghome_creation', + 'basic': set(['test_homedir_creation', + 'test_binary_discovery', 'test_gpg_binary', 'test_gpg_binary_not_abs', 'test_gpg_binary_version_str', diff --git a/gnupg/util.py b/gnupg/util.py index dd9d1f1..a4ddcfe 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -111,28 +111,29 @@ def _copy_data(instream, outstream): try: outstream.close() except IOError: - logger.exception('_copy_data(): Got IOError while closing %s' % outstream) + logger.exception('_copy_data(): Got IOError while closing %s' + % outstream) else: logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) -def _create_gpghome(gpghome): +def _create_homedir(homedir): """Create the specified GnuPG home directory, if necessary. - :param str gpghome: The directory to use. + :param str homedir: The directory to use. :rtype: bool :returns: True if no errors occurred and the directory was created or existed beforehand, False otherwise. """ ## xxx how will this work in a virtualenv? - if not os.path.isabs(gpghome): - message = ("Got non-abs gpg home dir path: %s" % gpghome) - logger.warn("util._create_gpghome(): %s" % message) - gpghome = os.path.abspath(gpghome) - if not os.path.isdir(gpghome): - message = ("Creating gpg home dir: %s" % gpghome) - logger.warn("util._create_gpghome(): %s" % message) + if not os.path.isabs(homedir): + message = ("Got non-abs gpg home dir path: %s" % homedir) + logger.warn("util._create_homedir(): %s" % message) + homedir = os.path.abspath(homedir) + if not os.path.isdir(homedir): + message = ("Creating gpg home dir: %s" % homedir) + logger.warn("util._create_homedir(): %s" % message) try: - os.makedirs(gpghome, 0x1C0) + os.makedirs(homedir, 0x1C0) except OSError as ose: logger.error(ose, exc_info=1) return False @@ -141,22 +142,22 @@ def _create_gpghome(gpghome): else: return True -def _find_gpgbinary(gpgbinary=None): +def _find_binary(binary=None): """Find the absolute path to the GnuPG binary. Also run checks that the binary is not a symlink, and check that our process real uid has exec permissions. - :param str gpgbinary: The path to the GnuPG binary. + :param str binary: The path to the GnuPG binary. :raises: :exc:RuntimeError if it appears that GnuPG is not installed. :rtype: str :returns: The absolute path to the GnuPG binary to use, if no exceptions occur. """ - binary = None - if gpgbinary is not None: - if not os.path.isabs(gpgbinary): - try: binary = _which(gpgbinary)[0] + gpg_binary = None + if binary is not None: + if not os.path.isabs(binary): + try: binary = _which(binary)[0] except IndexError as ie: logger.debug(ie.message) if binary is None: try: binary = _which('gpg')[0] @@ -166,7 +167,7 @@ def _find_gpgbinary(gpgbinary=None): assert not os.path.islink(binary), "Path to gpg binary is symlink" assert os.access(binary, os.X_OK), "Lacking +x perms for gpg binary" except (AssertionError, AttributeError) as ae: - logger.debug("util._find_gpgbinary(): %s" % ae.message) + logger.debug("util._find_binary(): %s" % ae.message) else: return binary