summaryrefslogtreecommitdiff
path: root/gnupg
diff options
context:
space:
mode:
authorKali Kaneko <kali@futeisha.org>2014-07-02 11:38:15 -0500
committerKali Kaneko <kali@futeisha.org>2014-07-02 11:38:15 -0500
commitd1955bd267a132c24d9e64dde7a1cdb8bd9fe9c5 (patch)
tree241d30509e30704431ddd378883e477eaa225a07 /gnupg
parentbc02d9b78a274f3d2a73c63473bc6b28cfcc5f8d (diff)
Imported Upstream version 1.2.6
Diffstat (limited to 'gnupg')
-rw-r--r--gnupg/__init__.py47
-rw-r--r--gnupg/_ansistrm.py172
-rw-r--r--gnupg/_logger.py99
-rw-r--r--gnupg/_meta.py871
-rw-r--r--gnupg/_parsers.py1385
-rw-r--r--gnupg/_trust.py103
-rw-r--r--gnupg/_util.py617
-rw-r--r--gnupg/_version.py11
-rw-r--r--gnupg/copyleft.py749
-rw-r--r--gnupg/gnupg.py1067
10 files changed, 5121 insertions, 0 deletions
diff --git a/gnupg/__init__.py b/gnupg/__init__.py
new file mode 100644
index 0000000..5c1430c
--- /dev/null
+++ b/gnupg/__init__.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+from __future__ import absolute_import
+
+from . import gnupg
+from . import copyleft
+from . import _ansistrm
+from . import _logger
+from . import _meta
+from . import _parsers
+from . import _util
+from .gnupg import GPG
+from ._version import get_versions
+
+__version__ = get_versions()['version']
+__authors__ = copyleft.authors
+__license__ = copyleft.full_text
+__copyleft__ = copyleft.copyright
+
+## do not set __package__ = "gnupg", else we will end up with
+## gnupg.<*allofthethings*>
+__all__ = ["GPG", "_util", "_parsers", "_meta", "_logger"]
+
+## avoid the "from gnupg import gnupg" idiom
+del gnupg
+del absolute_import
+del copyleft
+del get_versions
+del _version
diff --git a/gnupg/_ansistrm.py b/gnupg/_ansistrm.py
new file mode 100644
index 0000000..cfd50a1
--- /dev/null
+++ b/gnupg/_ansistrm.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python wrapper aroung GnuPG, and it was
+# taken from https://gist.github.com/vsajip/758430 on the 14th of May, 2013. It
+# has also been included in the 'logutils' Python module, see
+# https://code.google.com/p/logutils/ .
+#
+# The original copyright and license text are as follows:
+# |
+# | Copyright (C) 2010-2012 Vinay Sajip. All rights reserved.
+# | Licensed under the new BSD license.
+# |
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+import ctypes
+import logging
+import os
+
+class ColorizingStreamHandler(logging.StreamHandler):
+ # color names to indices
+ color_map = {
+ 'black': 0,
+ 'red': 1,
+ 'green': 2,
+ 'yellow': 3,
+ 'blue': 4,
+ 'magenta': 5,
+ 'cyan': 6,
+ 'white': 7,
+ }
+
+ #levels to (background, foreground, bold/intense)
+ if os.name == 'nt':
+ level_map = {
+ logging.DEBUG: (None, 'blue', True),
+ logging.INFO: (None, 'green', False),
+ logging.WARNING: (None, 'yellow', True),
+ logging.ERROR: (None, 'red', True),
+ logging.CRITICAL: ('red', 'white', True),
+ }
+ else:
+ level_map = {
+ logging.DEBUG: (None, 'blue', False),
+ logging.INFO: (None, 'green', False),
+ logging.WARNING: (None, 'yellow', False),
+ logging.ERROR: (None, 'red', False),
+ logging.CRITICAL: ('red', 'white', True),
+ }
+ csi = '\x1b['
+ reset = '\x1b[0m'
+
+ @property
+ def is_tty(self):
+ isatty = getattr(self.stream, 'isatty', None)
+ return isatty and isatty()
+
+ def emit(self, record):
+ try:
+ message = self.format(record)
+ stream = self.stream
+ if not self.is_tty:
+ stream.write(message)
+ else:
+ self.output_colorized(message)
+ stream.write(getattr(self, 'terminator', '\n'))
+ self.flush()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ self.handleError(record)
+
+ if os.name != 'nt':
+ def output_colorized(self, message):
+ self.stream.write(message)
+ else:
+ import re
+ ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m')
+
+ nt_color_map = {
+ 0: 0x00, # black
+ 1: 0x04, # red
+ 2: 0x02, # green
+ 3: 0x06, # yellow
+ 4: 0x01, # blue
+ 5: 0x05, # magenta
+ 6: 0x03, # cyan
+ 7: 0x07, # white
+ }
+
+ def output_colorized(self, message):
+ parts = self.ansi_esc.split(message)
+ write = self.stream.write
+ h = None
+ fd = getattr(self.stream, 'fileno', None)
+ if fd is not None:
+ fd = fd()
+ if fd in (1, 2): # stdout or stderr
+ h = ctypes.windll.kernel32.GetStdHandle(-10 - fd)
+ while parts:
+ text = parts.pop(0)
+ if text:
+ write(text)
+ if parts:
+ params = parts.pop(0)
+ if h is not None:
+ params = [int(p) for p in params.split(';')]
+ color = 0
+ for p in params:
+ if 40 <= p <= 47:
+ color |= self.nt_color_map[p - 40] << 4
+ elif 30 <= p <= 37:
+ color |= self.nt_color_map[p - 30]
+ elif p == 1:
+ color |= 0x08 # foreground intensity on
+ elif p == 0: # reset to default color
+ color = 0x07
+ else:
+ pass # error condition ignored
+ ctypes.windll.kernel32.SetConsoleTextAttribute(h, color)
+
+ def colorize(self, message, record):
+ if record.levelno in self.level_map:
+ bg, fg, bold = self.level_map[record.levelno]
+ params = []
+ if bg in self.color_map:
+ params.append(str(self.color_map[bg] + 40))
+ if fg in self.color_map:
+ params.append(str(self.color_map[fg] + 30))
+ if bold:
+ params.append('1')
+ if params:
+ message = ''.join((self.csi, ';'.join(params),
+ 'm', message, self.reset))
+ return message
+
+ def format(self, record):
+ message = logging.StreamHandler.format(self, record)
+ if self.is_tty:
+ # Don't colorize any traceback
+ parts = message.split('\n', 1)
+ parts[0] = self.colorize(parts[0], record)
+ message = '\n'.join(parts)
+ return message
+
+def main():
+ root = logging.getLogger()
+ root.setLevel(logging.DEBUG)
+ root.addHandler(ColorizingStreamHandler())
+ logging.debug('DEBUG')
+ logging.info('INFO')
+ logging.warning('WARNING')
+ logging.error('ERROR')
+ logging.critical('CRITICAL')
+
+if __name__ == '__main__':
+ main()
diff --git a/gnupg/_logger.py b/gnupg/_logger.py
new file mode 100644
index 0000000..870617e
--- /dev/null
+++ b/gnupg/_logger.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+'''Logging module for python-gnupg.'''
+
+from __future__ import absolute_import
+from __future__ import print_function
+from datetime import datetime
+from functools import wraps
+
+import logging
+import sys
+import os
+
+try:
+ from logging import NullHandler
+except:
+ class NullHandler(logging.Handler):
+ def handle(self, record):
+ pass
+
+from . import _ansistrm
+
+GNUPG_STATUS_LEVEL = 9
+
+def status(self, message, *args, **kwargs):
+ """LogRecord for GnuPG internal status messages."""
+ if self.isEnabledFor(GNUPG_STATUS_LEVEL):
+ self._log(GNUPG_STATUS_LEVEL, message, args, **kwargs)
+
+@wraps(logging.Logger)
+def create_logger(level=logging.NOTSET):
+ """Create a logger for python-gnupg at a specific message level.
+
+ :type level: :obj:`int` or :obj:`str`
+ :param level: A string or an integer for the lowest level to include in
+ logs.
+
+ **Available levels:**
+
+ ==== ======== ========================================
+ int str description
+ ==== ======== ========================================
+ 0 NOTSET Disable all logging.
+ 9 GNUPG Log GnuPG's internal status messages.
+ 10 DEBUG Log module level debuging messages.
+ 20 INFO Normal user-level messages.
+ 30 WARN Warning messages.
+ 40 ERROR Error messages and tracebacks.
+ 50 CRITICAL Unhandled exceptions and tracebacks.
+ ==== ======== ========================================
+ """
+ _test = os.path.join(os.path.join(os.getcwd(), 'gnupg'), 'test')
+ _now = datetime.now().strftime("%Y-%m-%d_%H%M%S")
+ _fn = os.path.join(_test, "%s_test_gnupg.log" % _now)
+ _fmt = "%(relativeCreated)-4d L%(lineno)-4d:%(funcName)-18.18s %(levelname)-7.7s %(message)s"
+
+ ## Add the GNUPG_STATUS_LEVEL LogRecord to all Loggers in the module:
+ logging.addLevelName(GNUPG_STATUS_LEVEL, "GNUPG")
+ logging.Logger.status = status
+
+ if level > logging.NOTSET:
+ logging.basicConfig(level=level, filename=_fn,
+ filemode="a", format=_fmt)
+ logging.logThreads = True
+ if hasattr(logging,'captureWarnings'):
+ logging.captureWarnings(True)
+ colouriser = _ansistrm.ColorizingStreamHandler
+ colouriser.level_map[9] = (None, 'blue', False)
+ colouriser.level_map[10] = (None, 'cyan', False)
+ handler = colouriser(sys.stderr)
+ handler.setLevel(level)
+
+ formatr = logging.Formatter(_fmt)
+ handler.setFormatter(formatr)
+ else:
+ handler = NullHandler()
+
+ log = logging.getLogger('gnupg')
+ log.addHandler(handler)
+ log.setLevel(level)
+ log.info("Log opened: %s UTC" % datetime.ctime(datetime.utcnow()))
+ return log
diff --git a/gnupg/_meta.py b/gnupg/_meta.py
new file mode 100644
index 0000000..f11310c
--- /dev/null
+++ b/gnupg/_meta.py
@@ -0,0 +1,871 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+'''Meta and base classes for hiding internal functions, and controlling
+attribute creation and handling.
+'''
+
+from __future__ import absolute_import
+
+import atexit
+import codecs
+import encodings
+## For AOS, the locale module will need to point to a wrapper around the
+## java.util.Locale class.
+## See https://code.patternsinthevoid.net/?p=android-locale-hack.git
+import locale
+import os
+import psutil
+import subprocess
+import sys
+import threading
+
+from . import _parsers
+from . import _util
+
+from ._parsers import _check_preferences
+from ._parsers import _sanitise_list
+from ._util import log
+
+
+class GPGMeta(type):
+ """Metaclass for changing the :meth:GPG.__init__ initialiser.
+
+ Detects running gpg-agent processes and the presence of a pinentry
+ program, and disables pinentry so that python-gnupg can write the
+ passphrase to the controlled GnuPG process without killing the agent.
+
+ :attr _agent_proc: If a :program:`gpg-agent` process is currently running
+ for the effective userid, then **_agent_proc** will be
+ set to a ``psutil.Process`` for that process.
+ """
+
+ def __new__(cls, name, bases, attrs):
+ """Construct the initialiser for GPG"""
+ log.debug("Metaclass __new__ constructor called for %r" % cls)
+ if cls._find_agent():
+ ## call the normal GPG.__init__() initialiser:
+ attrs['init'] = cls.__init__
+ attrs['_remove_agent'] = True
+ return super(GPGMeta, cls).__new__(cls, name, bases, attrs)
+
+ @classmethod
+ def _find_agent(cls):
+ """Discover if a gpg-agent process for the current euid is running.
+
+ If there is a matching gpg-agent process, set a :class:`psutil.Process`
+ instance containing the gpg-agent process' information to
+ ``cls._agent_proc``.
+
+ :returns: True if there exists a gpg-agent process running under the
+ same effective user ID as that of this program. Otherwise,
+ returns None.
+ """
+ identity = psutil.Process(os.getpid()).uids
+ for proc in psutil.process_iter():
+ if (proc.name == "gpg-agent") and proc.is_running:
+ log.debug("Found gpg-agent process with pid %d" % proc.pid)
+ if proc.uids == identity:
+ log.debug(
+ "Effective UIDs of this process and gpg-agent match")
+ setattr(cls, '_agent_proc', proc)
+ return True
+
+
+class GPGBase(object):
+ """Base class for storing properties and controlling process initialisation.
+
+ :const _result_map: A *dict* containing classes from
+ :mod:`~gnupg._parsers`, used for parsing results
+ obtained from GnuPG commands.
+ :const _decode_errors: How to handle encoding errors.
+ """
+ __metaclass__ = GPGMeta
+ _decode_errors = 'strict'
+ _result_map = { 'crypt': _parsers.Crypt,
+ 'delete': _parsers.DeleteResult,
+ 'generate': _parsers.GenKey,
+ 'import': _parsers.ImportResult,
+ 'list': _parsers.ListKeys,
+ 'sign': _parsers.Sign,
+ 'verify': _parsers.Verify,
+ 'packets': _parsers.ListPackets }
+
+ def __init__(self, binary=None, home=None, keyring=None, secring=None,
+ use_agent=False, default_preference_list=None,
+ verbose=False, options=None):
+ """Create a ``GPGBase``.
+
+ This class is used to set up properties for controlling the behaviour
+ of configuring various options for GnuPG, such as setting GnuPG's
+ **homedir** , and the paths to its **binary** and **keyring** .
+
+ :const binary: (:obj:`str`) The full path to the GnuPG binary.
+
+ :ivar homedir: (:class:`~gnupg._util.InheritableProperty`) The full
+ path to the current setting for the GnuPG
+ ``--homedir``.
+
+ :ivar _generated_keys: (:class:`~gnupg._util.InheritableProperty`)
+ Controls setting the directory for storing any
+ keys which are generated with
+ :meth:`~gnupg.GPG.gen_key`.
+
+ :ivar str keyring: The filename in **homedir** to use as the keyring
+ file for public keys.
+ :ivar str secring: The filename in **homedir** to use as the keyring
+ file for secret keys.
+ """
+ self.binary = _util._find_binary(binary)
+ self.homedir = home if home else _util._conf
+ pub = _parsers._fix_unsafe(keyring) if keyring else 'pubring.gpg'
+ sec = _parsers._fix_unsafe(secring) if secring else 'secring.gpg'
+ self.keyring = os.path.join(self._homedir, pub)
+ self.secring = os.path.join(self._homedir, sec)
+ self.options = _parsers._sanitise(options) if options else None
+
+ if default_preference_list:
+ self._prefs = _check_preferences(default_preference_list, 'all')
+ else:
+ self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH'
+ self._prefs += ' AES192 ZLIB ZIP Uncompressed'
+
+ encoding = locale.getpreferredencoding()
+ if encoding is None: # This happens on Jython!
+ encoding = sys.stdin.encoding
+ self._encoding = encoding.lower().replace('-', '_')
+ self._filesystemencoding = encodings.normalize_encoding(
+ sys.getfilesystemencoding().lower())
+
+ self._keyserver = 'hkp://wwwkeys.pgp.net'
+ self.__generated_keys = os.path.join(self.homedir, 'generated-keys')
+
+ try:
+ assert self.binary, "Could not find binary %s" % binary
+ assert isinstance(verbose, (bool, str, int)), \
+ "'verbose' must be boolean, string, or 0 <= n <= 9"
+ assert isinstance(use_agent, bool), "'use_agent' must be boolean"
+ if self.options is not None:
+ assert isinstance(self.options, str), "options not string"
+ except (AssertionError, AttributeError) as ae:
+ log.error("GPGBase.__init__(): %s" % str(ae))
+ raise RuntimeError(str(ae))
+ else:
+ if verbose is True:
+ # The caller wants logging, but we need a valid --debug-level
+ # for gpg. Default to "basic", and warn about the ambiguity.
+ # (garrettr)
+ verbose = "basic"
+ log.warning('GPG(verbose=True) is ambiguous, defaulting to "basic" logging')
+ self.verbose = verbose
+ self.use_agent = use_agent
+
+ if hasattr(self, '_agent_proc') \
+ and getattr(self, '_remove_agent', None) is True:
+ if hasattr(self, '__remove_path__'):
+ self.__remove_path__('pinentry')
+
+ def __remove_path__(self, prog=None, at_exit=True):
+ """Remove the directories containing a program from the system's
+ ``$PATH``. If ``GPGBase.binary`` is in a directory being removed, it
+ is linked to :file:'./gpg' in the current directory.
+
+ :param str prog: The program to remove from ``$PATH``.
+ :param bool at_exit: Add the program back into the ``$PATH`` when the
+ Python interpreter exits, and delete any symlinks
+ to ``GPGBase.binary`` which were created.
+ """
+ #: A list of ``$PATH`` entries which were removed to disable pinentry.
+ self._removed_path_entries = []
+
+ log.debug("Attempting to remove %s from system PATH" % str(prog))
+ if (prog is None) or (not isinstance(prog, str)): return
+
+ try:
+ program = _util._which(prog)[0]
+ except (OSError, IOError, IndexError) as err:
+ log.err(str(err))
+ log.err("Cannot find program '%s', not changing PATH." % prog)
+ return
+
+ ## __remove_path__ cannot be an @classmethod in GPGMeta, because
+ ## the use_agent attribute must be set by the instance.
+ if not self.use_agent:
+ program_base = os.path.dirname(prog)
+ gnupg_base = os.path.dirname(self.binary)
+
+ ## symlink our gpg binary into $PWD if the path we are removing is
+ ## the one which contains our gpg executable:
+ new_gpg_location = os.path.join(os.getcwd(), 'gpg')
+ if gnupg_base == program_base:
+ os.symlink(self.binary, new_gpg_location)
+ self.binary = new_gpg_location
+
+ ## copy the original environment so that we can put it back later:
+ env_copy = os.environ ## this one should not be touched
+ path_copy = os.environ.pop('PATH')
+ log.debug("Created a copy of system PATH: %r" % path_copy)
+ assert not os.environ.has_key('PATH'), "OS env kept $PATH anyway!"
+
+ @staticmethod
+ def remove_program_from_path(path, prog_base):
+ """Remove all directories which contain a program from PATH.
+
+ :param str path: The contents of the system environment's
+ ``$PATH``.
+
+ :param str prog_base: The directory portion of a program's
+ location, without the trailing slash,
+ and without the program name. For
+ example, ``prog_base='/usr/bin'``.
+ """
+ paths = path.split(':')
+ for directory in paths:
+ if directory == prog_base:
+ log.debug("Found directory with target program: %s"
+ % directory)
+ path.remove(directory)
+ self._removed_path_entries.append(directory)
+ log.debug("Deleted all found instance of %s." % directory)
+ log.debug("PATH is now:%s%s" % (os.linesep, path))
+ new_path = ':'.join([p for p in path])
+ return new_path
+
+ @staticmethod
+ def update_path(environment, path):
+ """Add paths to the string at ``os.environ['PATH']``.
+
+ :param str environment: The environment mapping to update.
+ :param list path: A list of strings to update the PATH with.
+ """
+ log.debug("Updating system path...")
+ os.environ = environment
+ new_path = ':'.join([p for p in path])
+ old = ''
+ if 'PATH' in os.environ:
+ new_path = ':'.join([os.environ['PATH'], new_path])
+ os.environ.update({'PATH': new_path})
+ log.debug("System $PATH: %s" % os.environ['PATH'])
+
+ modified_path = remove_program_from_path(path_copy, program_base)
+ update_path(env_copy, modified_path)
+
+ ## register an _exithandler with the python interpreter:
+ atexit.register(update_path, env_copy, path_copy)
+
+ def remove_symlinked_binary(symlink):
+ if os.path.islink(symlink):
+ os.unlink(symlink)
+ log.debug("Removed binary symlink '%s'" % symlink)
+ atexit.register(remove_symlinked_binary, new_gpg_location)
+
+ @property
+ def default_preference_list(self):
+ """Get the default preference list."""
+ return self._prefs
+
+ @default_preference_list.setter
+ def default_preference_list(self, prefs):
+ """Set the default preference list.
+
+ :param str prefs: A string containing the default preferences for
+ ciphers, digests, and compression algorithms.
+ """
+ prefs = _check_preferences(prefs)
+ if prefs is not None:
+ self._prefs = prefs
+
+ @default_preference_list.deleter
+ def default_preference_list(self):
+ """Reset the default preference list to its original state.
+
+ Note that "original state" does not mean the default preference
+ list for whichever version of GnuPG is being used. It means the
+ default preference list defined by :attr:`GPGBase._prefs`.
+
+ Using BZIP2 is avoided due to not interacting well with some versions
+ of GnuPG>=2.0.0.
+ """
+ self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP'
+
+ @property
+ def keyserver(self):
+ """Get the current keyserver setting."""
+ return self._keyserver
+
+ @keyserver.setter
+ def keyserver(self, location):
+ """Set the default keyserver to use for sending and receiving keys.
+
+ The ``location`` is sent to :func:`_parsers._check_keyserver` when
+ option are parsed in :meth:`gnupg.GPG._make_options`.
+
+ :param str location: A string containing the default keyserver. This
+ should contain the desired keyserver protocol
+ which is supported by the keyserver, for example,
+ ``'hkps://keys.mayfirst.org'``. The default
+ keyserver is ``'hkp://wwwkeys.pgp.net'``.
+ """
+ self._keyserver = location
+
+ @keyserver.deleter
+ def keyserver(self):
+ """Reset the keyserver to the default setting."""
+ self._keyserver = 'hkp://wwwkeys.pgp.net'
+
+ def _homedir_getter(self):
+ """Get the directory currently being used as GnuPG's homedir.
+
+ If unspecified, use :file:`~/.config/python-gnupg/`
+
+ :rtype: str
+ :returns: The absolute path to the current GnuPG homedir.
+ """
+ return self._homedir
+
+ def _homedir_setter(self, directory):
+ """Set the directory to use as GnuPG's homedir.
+
+ If unspecified, use $HOME/.config/python-gnupg. If specified, ensure
+ that the ``directory`` does not contain various shell escape
+ characters. If ``directory`` is not found, it will be automatically
+ created. Lastly, the ``direcory`` will be checked that the EUID has
+ read and write permissions for it.
+
+ :param str directory: A relative or absolute path to the directory to
+ use for storing/accessing GnuPG's files, including
+ keyrings and the trustdb.
+ :raises: :exc:`~exceptions.RuntimeError` if unable to find a suitable
+ directory to use.
+ """
+ if not directory:
+ log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'"
+ % _util._conf)
+ directory = _util._conf
+
+ hd = _parsers._fix_unsafe(directory)
+ log.debug("GPGBase._homedir_setter(): got directory '%s'" % hd)
+
+ if hd:
+ log.debug("GPGBase._homedir_setter(): Check existence of '%s'" % hd)
+ _util._create_if_necessary(hd)
+
+ try:
+ log.debug("GPGBase._homedir_setter(): checking permissions")
+ assert _util._has_readwrite(hd), \
+ "Homedir '%s' needs read/write permissions" % hd
+ except AssertionError as ae:
+ msg = ("Unable to set '%s' as GnuPG homedir" % directory)
+ log.debug("GPGBase.homedir.setter(): %s" % msg)
+ log.debug(str(ae))
+ raise RuntimeError(str(ae))
+ else:
+ log.info("Setting homedir to '%s'" % hd)
+ self._homedir = hd
+
+ homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter)
+
+ def _generated_keys_getter(self):
+ """Get the ``homedir`` subdirectory for storing generated keys.
+
+ :rtype: str
+ :returns: The absolute path to the current GnuPG homedir.
+ """
+ return self.__generated_keys
+
+ def _generated_keys_setter(self, directory):
+ """Set the directory for storing generated keys.
+
+ If unspecified, use
+ :meth:`~gnupg._meta.GPGBase.homedir`/generated-keys. If specified,
+ ensure that the ``directory`` does not contain various shell escape
+ characters. If ``directory`` isn't found, it will be automatically
+ created. Lastly, the ``directory`` will be checked to ensure that the
+ current EUID has read and write permissions for it.
+
+ :param str directory: A relative or absolute path to the directory to
+ use for storing/accessing GnuPG's files, including keyrings and
+ the trustdb.
+ :raises: :exc:`~exceptions.RuntimeError` if unable to find a suitable
+ directory to use.
+ """
+ if not directory:
+ directory = os.path.join(self.homedir, 'generated-keys')
+ log.debug("GPGBase._generated_keys_setter(): Using '%s'"
+ % directory)
+
+ hd = _parsers._fix_unsafe(directory)
+ log.debug("GPGBase._generated_keys_setter(): got directory '%s'" % hd)
+
+ if hd:
+ log.debug("GPGBase._generated_keys_setter(): Check exists '%s'"
+ % hd)
+ _util._create_if_necessary(hd)
+
+ try:
+ log.debug("GPGBase._generated_keys_setter(): check permissions")
+ assert _util._has_readwrite(hd), \
+ "Keys dir '%s' needs read/write permissions" % hd
+ except AssertionError as ae:
+ msg = ("Unable to set '%s' as generated keys dir" % directory)
+ log.debug("GPGBase._generated_keys_setter(): %s" % msg)
+ log.debug(str(ae))
+ raise RuntimeError(str(ae))
+ else:
+ log.info("Setting homedir to '%s'" % hd)
+ self.__generated_keys = hd
+
+ _generated_keys = _util.InheritableProperty(_generated_keys_getter,
+ _generated_keys_setter)
+
+ def _make_args(self, args, passphrase=False):
+ """Make a list of command line elements for GPG.
+
+ The value of ``args`` will be appended only if it passes the checks in
+ :func:`gnupg._parsers._sanitise`. The ``passphrase`` argument needs to
+ be True if a passphrase will be sent to GnuPG, else False.
+
+ :param list args: A list of strings of options and flags to pass to
+ ``GPG.binary``. This is input safe, meaning that
+ these values go through strict checks (see
+ ``parsers._sanitise_list``) before being passed to to
+ the input file descriptor for the GnuPG process.
+ Each string should be given exactly as it would be on
+ the commandline interface to GnuPG,
+ e.g. ["--cipher-algo AES256", "--default-key
+ A3ADB67A2CDB8B35"].
+
+ :param bool passphrase: If True, the passphrase will be sent to the
+ stdin file descriptor for the attached GnuPG
+ process.
+ """
+ ## see TODO file, tag :io:makeargs:
+ cmd = [self.binary,
+ '--no-options --no-emit-version --no-tty --status-fd 2']
+
+ if self.homedir: cmd.append('--homedir "%s"' % self.homedir)
+
+ if self.keyring:
+ cmd.append('--no-default-keyring --keyring %s' % self.keyring)
+ if self.secring:
+ cmd.append('--secret-keyring %s' % self.secring)
+
+ if passphrase: cmd.append('--batch --passphrase-fd 0')
+
+ if self.use_agent: cmd.append('--use-agent')
+ else: cmd.append('--no-use-agent')
+
+ if self.options:
+ [cmd.append(opt) for opt in iter(_sanitise_list(self.options))]
+ if args:
+ [cmd.append(arg) for arg in iter(_sanitise_list(args))]
+
+ if self.verbose:
+ cmd.append('--debug-all')
+ if ((isinstance(self.verbose, str) and
+ self.verbose in ['basic', 'advanced', 'expert', 'guru'])
+ or (isinstance(self.verbose, int) and (1<=self.verbose<=9))):
+ cmd.append('--debug-level %s' % self.verbose)
+
+ return cmd
+
+ def _open_subprocess(self, args=None, passphrase=False):
+ """Open a pipe to a GPG subprocess and return the file objects for
+ communicating with it.
+
+ :param list args: A list of strings of options and flags to pass to
+ ``GPG.binary``. This is input safe, meaning that
+ these values go through strict checks (see
+ ``parsers._sanitise_list``) before being passed to to
+ the input file descriptor for the GnuPG process.
+ Each string should be given exactly as it would be on
+ the commandline interface to GnuPG,
+ e.g. ["--cipher-algo AES256", "--default-key
+ A3ADB67A2CDB8B35"].
+
+ :param bool passphrase: If True, the passphrase will be sent to the
+ stdin file descriptor for the attached GnuPG
+ process.
+ """
+ ## see http://docs.python.org/2/library/subprocess.html#converting-an\
+ ## -argument-sequence-to-a-string-on-windows
+ cmd = ' '.join(self._make_args(args, passphrase))
+ log.debug("Sending command to GnuPG process:%s%s" % (os.linesep, cmd))
+ return subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ env={'LANGUAGE': 'en'})
+
+ def _read_response(self, stream, result):
+ """Reads all the stderr output from GPG, taking notice only of lines
+ that begin with the magic [GNUPG:] prefix.
+
+ Calls methods on the response object for each valid token found, with
+ the arg being the remainder of the status line.
+
+ :param stream: A byte-stream, file handle, or a
+ :data:`subprocess.PIPE` for parsing the status codes
+ from the GnuPG process.
+
+ :param result: The result parser class from :mod:`~gnupg._parsers` ―
+ the ``handle_status()`` method of that class will be
+ called in order to parse the output of ``stream``.
+ """
+ lines = []
+ while True:
+ line = stream.readline()
+ if len(line) == 0:
+ break
+ lines.append(line)
+ line = line.rstrip()
+
+ if line.startswith('[GNUPG:]'):
+ line = _util._deprefix(line, '[GNUPG:] ', log.status)
+ keyword, value = _util._separate_keyword(line)
+ result._handle_status(keyword, value)
+ elif line.startswith('gpg:'):
+ line = _util._deprefix(line, 'gpg: ')
+ keyword, value = _util._separate_keyword(line)
+
+ # Log gpg's userland messages at our own levels:
+ if keyword.upper().startswith("WARNING"):
+ log.warn("%s" % value)
+ elif keyword.upper().startswith("FATAL"):
+ log.critical("%s" % value)
+ # Handle the gpg2 error where a missing trustdb.gpg is,
+ # for some stupid reason, considered fatal:
+ if value.find("trustdb.gpg") and value.find("No such file"):
+ result._handle_status('NEED_TRUSTDB', '')
+ else:
+ if self.verbose:
+ log.info("%s" % line)
+ else:
+ log.debug("%s" % line)
+ result.stderr = ''.join(lines)
+
+ def _read_data(self, stream, result):
+ """Incrementally read from ``stream`` and store read data.
+
+ All data gathered from calling ``stream.read()`` will be concatenated
+ and stored as ``result.data``.
+
+ :param stream: An open file-like object to read() from.
+ :param result: An instance of one of the :ref:`result parsing classes
+ <parsers>` from :const:`~gnupg._meta.GPGBase._result_map`.
+ """
+ chunks = []
+ log.debug("Reading data from stream %r..." % stream.__repr__())
+
+ while True:
+ data = stream.read(1024)
+ if len(data) == 0:
+ break
+ chunks.append(data)
+ log.debug("Read %4d bytes" % len(data))
+
+ # Join using b'' or '', as appropriate
+ result.data = type(data)().join(chunks)
+ log.debug("Finishing reading from stream %r..." % stream.__repr__())
+ log.debug("Read %4d bytes total" % len(result.data))
+
+ def _collect_output(self, process, result, writer=None, stdin=None):
+ """Drain the subprocesses output streams, writing the collected output
+ to the result. If a writer thread (writing to the subprocess) is given,
+ make sure it's joined before returning. If a stdin stream is given,
+ close it before returning.
+ """
+ stderr = codecs.getreader(self._encoding)(process.stderr)
+ rr = threading.Thread(target=self._read_response,
+ args=(stderr, result))
+ rr.setDaemon(True)
+ log.debug('stderr reader: %r', rr)
+ rr.start()
+
+ stdout = process.stdout
+ dr = threading.Thread(target=self._read_data, args=(stdout, result))
+ dr.setDaemon(True)
+ log.debug('stdout reader: %r', dr)
+ dr.start()
+
+ dr.join()
+ rr.join()
+ if writer is not None:
+ writer.join()
+ process.wait()
+ if stdin is not None:
+ try:
+ stdin.close()
+ except IOError:
+ pass
+ stderr.close()
+ stdout.close()
+
+ def _handle_io(self, args, file, result, passphrase=False, binary=False):
+ """Handle a call to GPG - pass input data, collect output data."""
+ p = self._open_subprocess(args, passphrase)
+ if not binary:
+ stdin = codecs.getwriter(self._encoding)(p.stdin)
+ else:
+ stdin = p.stdin
+ if passphrase:
+ _util._write_passphrase(stdin, passphrase, self._encoding)
+ writer = _util._threaded_copy_data(file, stdin)
+ self._collect_output(p, result, writer, stdin)
+ return result
+
+ def _recv_keys(self, keyids, keyserver=None):
+ """Import keys from a keyserver.
+
+ :param str keyids: A space-delimited string containing the keyids to
+ request.
+ :param str keyserver: The keyserver to request the ``keyids`` from;
+ defaults to `gnupg.GPG.keyserver`.
+ """
+ if not keyserver:
+ keyserver = self.keyserver
+
+ args = ['--keyserver {0}'.format(keyserver),
+ '--recv-keys {0}'.format(keyids)]
+ log.info('Requesting keys from %s: %s' % (keyserver, keyids))
+
+ result = self._result_map['import'](self)
+ proc = self._open_subprocess(args)
+ self._collect_output(proc, result)
+ log.debug('recv_keys result: %r', result.__dict__)
+ return result
+
+ def _sign_file(self, file, default_key=None, passphrase=None,
+ clearsign=True, detach=False, binary=False,
+ digest_algo='SHA512'):
+ """Create a signature for a file.
+
+ :param file: The file stream (i.e. it's already been open()'d) to sign.
+ :param str default_key: The key to sign with.
+ :param str passphrase: The passphrase to pipe to stdin.
+ :param bool clearsign: If True, create a cleartext signature.
+ :param bool detach: If True, create a detached signature.
+ :param bool binary: If True, do not ascii armour the output.
+ :param str digest_algo: The hash digest to use. Again, to see which
+ hashes your GnuPG is capable of using, do:
+ ``$ gpg --with-colons --list-config
+ digestname``. The default, if unspecified, is
+ ``'SHA512'``.
+ """
+ log.debug("_sign_file():")
+ if binary:
+ log.info("Creating binary signature for file %s" % file)
+ args = ['--sign']
+ else:
+ log.info("Creating ascii-armoured signature for file %s" % file)
+ args = ['--sign --armor']
+
+ if clearsign:
+ args.append("--clearsign")
+ if detach:
+ log.warn("Cannot use both --clearsign and --detach-sign.")
+ log.warn("Using default GPG behaviour: --clearsign only.")
+ elif detach and not clearsign:
+ args.append("--detach-sign")
+
+ if default_key:
+ args.append(str("--default-key %s" % default_key))
+
+ args.append(str("--digest-algo %s" % digest_algo))
+
+ ## We could use _handle_io here except for the fact that if the
+ ## passphrase is bad, gpg bails and you can't write the message.
+ result = self._result_map['sign'](self)
+ proc = self._open_subprocess(args, passphrase is not None)
+ try:
+ if passphrase:
+ _util._write_passphrase(proc.stdin, passphrase, self._encoding)
+ writer = _util._threaded_copy_data(file, proc.stdin)
+ except IOError as ioe:
+ log.exception("Error writing message: %s" % str(ioe))
+ writer = None
+ self._collect_output(proc, result, writer, proc.stdin)
+ return result
+
+ def _encrypt(self, data, recipients,
+ default_key=None,
+ passphrase=None,
+ armor=True,
+ encrypt=True,
+ symmetric=False,
+ always_trust=True,
+ output=None,
+ cipher_algo='AES256',
+ digest_algo='SHA512',
+ compress_algo='ZLIB'):
+ """Encrypt the message read from the file-like object **data**.
+
+ :param str data: The file or bytestream to encrypt.
+
+ :param str recipients: The recipients to encrypt to. Recipients must
+ be specified keyID/fingerprint.
+
+ .. warning:: Care should be taken in Python2 to make sure that the
+ given fingerprints for **recipients** are in fact strings
+ and not unicode objects.
+
+ :param str default_key: The keyID/fingerprint of the key to use for
+ signing. If given, **data** will be encrypted
+ *and* signed.
+
+ :param str passphrase: If given, and **default_key** is also given,
+ use this passphrase to unlock the secret
+ portion of the **default_key** to sign the
+ encrypted **data**. Otherwise, if
+ **default_key** is not given, but **symmetric**
+ is ``True``, then use this passphrase as the
+ passphrase for symmetric encryption. Signing
+ and symmetric encryption should *not* be
+ combined when sending the **data** to other
+ recipients, else the passphrase to the secret
+ key would be shared with them.
+
+ :param bool armor: If True, ascii armor the output; otherwise, the
+ output will be in binary format. (Default: True)
+
+ :param bool encrypt: If True, encrypt the **data** using the
+ **recipients** public keys. (Default: True)
+
+ :param bool symmetric: If True, encrypt the **data** to **recipients**
+ using a symmetric key. See the **passphrase**
+ parameter. Symmetric encryption and public key
+ encryption can be used simultaneously, and will
+ result in a ciphertext which is decryptable
+ with either the symmetric **passphrase** or one
+ of the corresponding private keys.
+
+ :param bool always_trust: If True, ignore trust warnings on
+ **recipients** keys. If False, display trust
+ warnings. (default: True)
+
+ :param str output: The output file to write to. If not specified, the
+ encrypted output is returned, and thus should be
+ stored as an object in Python. For example:
+
+
+ >>> import shutil
+ >>> import gnupg
+ >>> if os.path.exists("doctests"):
+ ... shutil.rmtree("doctests")
+ >>> gpg = gnupg.GPG(homedir="doctests")
+ >>> key_settings = gpg.gen_key_input(key_type='RSA',
+ ... key_length=1024,
+ ... key_usage='ESCA',
+ ... passphrase='foo')
+ >>> key = gpg.gen_key(key_settings)
+ >>> message = "The crow flies at midnight."
+ >>> encrypted = str(gpg.encrypt(message, key.printprint))
+ >>> assert encrypted != message
+ >>> assert not encrypted.isspace()
+ >>> decrypted = str(gpg.decrypt(encrypted))
+ >>> assert not decrypted.isspace()
+ >>> decrypted
+ 'The crow flies at midnight.'
+
+ :param str cipher_algo: The cipher algorithm to use. To see available
+ algorithms with your version of GnuPG, do:
+ :command:`$ gpg --with-colons --list-config
+ ciphername`. The default **cipher_algo**, if
+ unspecified, is ``'AES256'``.
+
+ :param str digest_algo: The hash digest to use. Again, to see which
+ hashes your GnuPG is capable of using, do:
+ :command:`$ gpg --with-colons --list-config
+ digestname`. The default, if unspecified, is
+ ``'SHA512'``.
+
+ :param str compress_algo: The compression algorithm to use. Can be one
+ of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or
+ ``'Uncompressed'``.
+ """
+ args = []
+
+ if output:
+ if getattr(output, 'fileno', None) is not None:
+ ## avoid overwrite confirmation message
+ if getattr(output, 'name', None) is None:
+ if os.path.exists(output):
+ os.remove(output)
+ args.append('--output %s' % output)
+ else:
+ if os.path.exists(output.name):
+ os.remove(output.name)
+ args.append('--output %s' % output.name)
+
+ if armor: args.append('--armor')
+ if always_trust: args.append('--always-trust')
+ if cipher_algo: args.append('--cipher-algo %s' % cipher_algo)
+ if compress_algo: args.append('--compress-algo %s' % compress_algo)
+
+ if default_key:
+ args.append('--sign')
+ args.append('--default-key %s' % default_key)
+ if digest_algo:
+ args.append('--digest-algo %s' % digest_algo)
+
+ ## both can be used at the same time for an encrypted file which
+ ## is decryptable with a passphrase or secretkey.
+ if symmetric: args.append('--symmetric')
+ if encrypt: args.append('--encrypt')
+
+ if len(recipients) >= 1:
+ log.debug("GPG.encrypt() called for recipients '%s' with type '%s'"
+ % (recipients, type(recipients)))
+
+ if isinstance(recipients, (list, tuple)):
+ for recp in recipients:
+ if not _util._py3k:
+ if isinstance(recp, unicode):
+ try:
+ assert _parsers._is_hex(str(recp))
+ except AssertionError:
+ log.info("Can't accept recipient string: %s"
+ % recp)
+ else:
+ args.append('--recipient %s' % str(recp))
+ continue
+ ## will give unicode in 2.x as '\uXXXX\uXXXX'
+ args.append('--recipient %r' % recp)
+ continue
+ if isinstance(recp, str):
+ args.append('--recipient %s' % recp)
+
+ elif (not _util._py3k) and isinstance(recp, basestring):
+ for recp in recipients.split('\x20'):
+ args.append('--recipient %s' % recp)
+
+ elif _util._py3k and isinstance(recp, str):
+ for recp in recipients.split(' '):
+ args.append('--recipient %s' % recp)
+ ## ...and now that we've proven py3k is better...
+
+ else:
+ log.debug("Don't know what to do with recipients: '%s'"
+ % recipients)
+
+ result = self._result_map['crypt'](self)
+ log.debug("Got data '%s' with type '%s'."
+ % (data, type(data)))
+ self._handle_io(args, data, result,
+ passphrase=passphrase, binary=True)
+ log.debug("\n%s" % result.data)
+ return result
diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py
new file mode 100644
index 0000000..2e1767e
--- /dev/null
+++ b/gnupg/_parsers.py
@@ -0,0 +1,1385 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+'''Classes for parsing GnuPG status messages and sanitising commandline
+options.
+'''
+
+from __future__ import absolute_import
+from __future__ import print_function
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ from ordereddict import OrderedDict
+
+import re
+
+from . import _util
+from ._util import log
+
+
+ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I)
+HEXIDECIMAL = re.compile('([0-9A-Fa-f]{2})+')
+
+
+class ProtectedOption(Exception):
+ """Raised when the option passed to GPG is disallowed."""
+
+class UsageError(Exception):
+ """Raised when incorrect usage of the API occurs.."""
+
+
+def _check_keyserver(location):
+ """Check that a given keyserver is a known protocol and does not contain
+ shell escape characters.
+
+ :param str location: A string containing the default keyserver. This
+ should contain the desired keyserver protocol which
+ is supported by the keyserver, for example, the
+ default is ``'hkp://wwwkeys .pgp.net'``.
+ :rtype: :obj:`str` or :obj:`None`
+ :returns: A string specifying the protocol and keyserver hostname, if the
+ checks passed. If not, returns None.
+ """
+ protocols = ['hkp://', 'hkps://', 'http://', 'https://', 'ldap://',
+ 'mailto:'] ## xxx feels like i´m forgetting one...
+ for proto in protocols:
+ if location.startswith(proto):
+ url = location.replace(proto, str())
+ host, slash, extra = url.partition('/')
+ if extra: log.warn("URI text for %s: '%s'" % (host, extra))
+ log.debug("Got host string for keyserver setting: '%s'" % host)
+
+ host = _fix_unsafe(host)
+ if host:
+ log.debug("Cleaned host string: '%s'" % host)
+ keyserver = proto + host
+ return keyserver
+ return None
+
+def _check_preferences(prefs, pref_type=None):
+ """Check cipher, digest, and compression preference settings.
+
+ MD5 is not allowed. This is `not 1994`__. SHA1 is allowed_ grudgingly_.
+
+ __ http://www.cs.colorado.edu/~jrblack/papers/md5e-full.pdf
+ .. _allowed: http://eprint.iacr.org/2008/469.pdf
+ .. _grudgingly: https://www.schneier.com/blog/archives/2012/10/when_will_we_se.html
+ """
+ if prefs is None: return
+
+ cipher = frozenset(['AES256', 'AES192', 'AES128',
+ 'CAMELLIA256', 'CAMELLIA192',
+ 'TWOFISH', '3DES'])
+ digest = frozenset(['SHA512', 'SHA384', 'SHA256', 'SHA224', 'RMD160',
+ 'SHA1'])
+ compress = frozenset(['BZIP2', 'ZLIB', 'ZIP', 'Uncompressed'])
+ all = frozenset([cipher, digest, compress])
+
+ if isinstance(prefs, str):
+ prefs = set(prefs.split())
+ elif isinstance(prefs, list):
+ prefs = set(prefs)
+ else:
+ msg = "prefs must be list of strings, or space-separated string"
+ log.error("parsers._check_preferences(): %s" % message)
+ raise TypeError(message)
+
+ if not pref_type:
+ pref_type = 'all'
+
+ allowed = str()
+
+ if pref_type == 'cipher':
+ allowed += ' '.join(prefs.intersection(cipher))
+ if pref_type == 'digest':
+ allowed += ' '.join(prefs.intersection(digest))
+ if pref_type == 'compress':
+ allowed += ' '.join(prefs.intersection(compress))
+ if pref_type == 'all':
+ allowed += ' '.join(prefs.intersection(all))
+
+ return allowed
+
+def _fix_unsafe(shell_input):
+ """Find characters used to escape from a string into a shell, and wrap them in
+ quotes if they exist. Regex pilfered from Python3 :mod:`shlex` module.
+
+ :param str shell_input: The input intended for the GnuPG process.
+ """
+ _unsafe = re.compile(r'[^\w@%+=:,./-]', 256)
+ try:
+ if len(_unsafe.findall(shell_input)) == 0:
+ return shell_input.strip()
+ else:
+ clean = "'" + shell_input.replace("'", "'\"'\"'") + "'"
+ return clean
+ except TypeError:
+ return None
+
+def _hyphenate(input, add_prefix=False):
+ """Change underscores to hyphens so that object attributes can be easily
+ tranlated to GPG option names.
+
+ :param str input: The attribute to hyphenate.
+ :param bool add_prefix: If True, add leading hyphens to the input.
+ :rtype: str
+ :return: The ``input`` with underscores changed to hyphens.
+ """
+ ret = '--' if add_prefix else ''
+ ret += input.replace('_', '-')
+ return ret
+
+def _is_allowed(input):
+ """Check that an option or argument given to GPG is in the set of allowed
+ options, the latter being a strict subset of the set of all options known
+ to GPG.
+
+ :param str input: An input meant to be parsed as an option or flag to the
+ GnuPG process. Should be formatted the same as an option
+ or flag to the commandline gpg, i.e. "--encrypt-files".
+
+ :ivar frozenset gnupg_options: All known GPG options and flags.
+
+ :ivar frozenset allowed: All allowed GPG options and flags, e.g. all GPG
+ options and flags which we are willing to
+ acknowledge and parse. If we want to support a
+ new option, it will need to have its own parsing
+ class and its name will need to be added to this
+ set.
+
+ :raises: :exc:`UsageError` if **input** is not a subset of the hard-coded
+ set of all GnuPG options in :func:`_get_all_gnupg_options`.
+
+ :exc:`ProtectedOption` if **input** is not in the set of allowed
+ options.
+
+ :rtype: str
+ :return: The original **input** parameter, unmodified and unsanitized, if
+ no errors occur.
+ """
+ gnupg_options = _get_all_gnupg_options()
+ allowed = _get_options_group("allowed")
+
+ ## these are the allowed options we will handle so far, all others should
+ ## be dropped. this dance is so that when new options are added later, we
+ ## merely add the to the _allowed list, and the `` _allowed.issubset``
+ ## assertion will check that GPG will recognise them
+ try:
+ ## check that allowed is a subset of all gnupg_options
+ assert allowed.issubset(gnupg_options)
+ except AssertionError:
+ raise UsageError("'allowed' isn't a subset of known options, diff: %s"
+ % allowed.difference(gnupg_options))
+
+ ## if we got a list of args, join them
+ ##
+ ## see TODO file, tag :cleanup:
+ if not isinstance(input, str):
+ input = ' '.join([x for x in input])
+
+ if isinstance(input, str):
+ if input.find('_') > 0:
+ if not input.startswith('--'):
+ hyphenated = _hyphenate(input, add_prefix=True)
+ else:
+ hyphenated = _hyphenate(input)
+ else:
+ hyphenated = input
+ ## xxx we probably want to use itertools.dropwhile here
+ try:
+ assert hyphenated in allowed
+ except AssertionError as ae:
+ dropped = _fix_unsafe(hyphenated)
+ log.warn("_is_allowed(): Dropping option '%s'..." % dropped)
+ raise ProtectedOption("Option '%s' not supported." % dropped)
+ else:
+ return input
+ return None
+
+def _is_hex(string):
+ """Check that a string is hexidecimal, with alphabetic characters
+ capitalized and without whitespace.
+
+ :param str string: The string to check.
+ """
+ matched = HEXIDECIMAL.match(string)
+ if matched is not None and len(matched.group()) >= 2:
+ return True
+ return False
+
+def _is_string(thing):
+ """Python character arrays are a mess.
+
+ If Python2, check if **thing** is an :obj:`unicode` or a :obj:`str`.
+ If Python3, check if **thing** is a :obj:`str`.
+
+ :param thing: The thing to check.
+ :returns: ``True`` if **thing** is a string according to whichever version
+ of Python we're running in.
+ """
+ if _util._py3k: return isinstance(thing, str)
+ else: return isinstance(thing, basestring)
+
+def _sanitise(*args):
+ """Take an arg or the key portion of a kwarg and check that it is in the
+ set of allowed GPG options and flags, and that it has the correct
+ type. Then, attempt to escape any unsafe characters. If an option is not
+ allowed, drop it with a logged warning. Returns a dictionary of all
+ sanitised, allowed options.
+
+ Each new option that we support that is not a boolean, but instead has
+ some additional inputs following it, i.e. "--encrypt-file foo.txt", will
+ need some basic safety checks added here.
+
+ GnuPG has three-hundred and eighteen commandline flags. Also, not all
+ implementations of OpenPGP parse PGP packets and headers in the same way,
+ so there is added potential there for messing with calls to GPG.
+
+ For information on the PGP message format specification, see
+ :rfc:`1991`.
+
+ If you're asking, "Is this *really* necessary?": No, not really -- we could
+ just follow the security precautions recommended by `this xkcd`__.
+
+ __ https://xkcd.com/1181/
+
+ :param str args: (optional) The boolean arguments which will be passed to
+ the GnuPG process.
+ :rtype: str
+ :returns: ``sanitised``
+ """
+
+ ## see TODO file, tag :cleanup:sanitise:
+
+ def _check_option(arg, value):
+ """Check that a single ``arg`` is an allowed option.
+
+ If it is allowed, quote out any escape characters in ``value``, and
+ add the pair to :ivar:`sanitised`. Otherwise, drop them.
+
+ :param str arg: The arguments which will be passed to the GnuPG
+ process, and, optionally their corresponding values.
+ The values are any additional arguments following the
+ GnuPG option or flag. For example, if we wanted to
+ pass ``"--encrypt --recipient isis@leap.se"`` to
+ GnuPG, then ``"--encrypt"`` would be an arg without a
+ value, and ``"--recipient"`` would also be an arg,
+ with a value of ``"isis@leap.se"``.
+
+ :ivar list checked: The sanitised, allowed options and values.
+ :rtype: str
+ :returns: A string of the items in ``checked``, delimited by spaces.
+ """
+ checked = str()
+ none_options = _get_options_group("none_options")
+ hex_options = _get_options_group("hex_options")
+ hex_or_none_options = _get_options_group("hex_or_none_options")
+
+ if not _util._py3k:
+ if not isinstance(arg, list) and isinstance(arg, unicode):
+ arg = str(arg)
+
+ try:
+ flag = _is_allowed(arg)
+ assert flag is not None, "_check_option(): got None for flag"
+ except (AssertionError, ProtectedOption) as error:
+ log.warn("_check_option(): %s" % str(error))
+ else:
+ checked += (flag + ' ')
+
+ if _is_string(value):
+ values = value.split(' ')
+ for v in values:
+ ## these can be handled separately, without _fix_unsafe(),
+ ## because they are only allowed if they pass the regex
+ if (flag in none_options) and (v is None):
+ continue
+
+ if flag in hex_options:
+ if _is_hex(v): checked += (v + " ")
+ else:
+ log.debug("'%s %s' not hex." % (flag, v))
+ if (flag in hex_or_none_options) and (v is None):
+ log.debug("Allowing '%s' for all keys" % flag)
+ continue
+
+ elif flag in ['--keyserver']:
+ host = _check_keyserver(v)
+ if host:
+ log.debug("Setting keyserver: %s" % host)
+ checked += (v + " ")
+ else: log.debug("Dropping keyserver: %s" % v)
+ continue
+
+ ## the rest are strings, filenames, etc, and should be
+ ## shell escaped:
+ val = _fix_unsafe(v)
+ try:
+ assert not val is None
+ assert not val.isspace()
+ assert not v is None
+ assert not v.isspace()
+ except:
+ log.debug("Dropping %s %s" % (flag, v))
+ continue
+
+ if flag in ['--encrypt', '--encrypt-files', '--decrypt',
+ '--decrypt-files', '--import', '--verify']:
+ if ( (_util._is_file(val))
+ or
+ ((flag == '--verify') and (val == '-')) ):
+ checked += (val + " ")
+ else:
+ log.debug("%s not file: %s" % (flag, val))
+
+ elif flag in ['--cipher-algo', '--personal-cipher-prefs',
+ '--personal-cipher-preferences']:
+ legit_algos = _check_preferences(val, 'cipher')
+ if legit_algos: checked += (legit_algos + " ")
+ else: log.debug("'%s' is not cipher" % val)
+
+ elif flag in ['--compress-algo', '--compression-algo',
+ '--personal-compress-prefs',
+ '--personal-compress-preferences']:
+ legit_algos = _check_preferences(val, 'compress')
+ if legit_algos: checked += (legit_algos + " ")
+ else: log.debug("'%s' not compress algo" % val)
+
+ else:
+ checked += (val + " ")
+ log.debug("_check_option(): No checks for %s" % val)
+
+ return checked
+
+ is_flag = lambda x: x.startswith('--')
+
+ def _make_filo(args_string):
+ filo = arg.split(' ')
+ filo.reverse()
+ log.debug("_make_filo(): Converted to reverse list: %s" % filo)
+ return filo
+
+ def _make_groups(filo):
+ groups = {}
+ while len(filo) >= 1:
+ last = filo.pop()
+ if is_flag(last):
+ log.debug("Got arg: %s" % last)
+ if last == '--verify':
+ groups[last] = str(filo.pop())
+ ## accept the read-from-stdin arg:
+ if len(filo) >= 1 and filo[len(filo)-1] == '-':
+ groups[last] += str(' - ') ## gross hack
+ filo.pop()
+ else:
+ groups[last] = str()
+ while len(filo) > 1 and not is_flag(filo[len(filo)-1]):
+ log.debug("Got value: %s" % filo[len(filo)-1])
+ groups[last] += (filo.pop() + " ")
+ else:
+ if len(filo) == 1 and not is_flag(filo[0]):
+ log.debug("Got value: %s" % filo[0])
+ groups[last] += filo.pop()
+ else:
+ log.warn("_make_groups(): Got solitary value: %s" % last)
+ groups["xxx"] = last
+ return groups
+
+ def _check_groups(groups):
+ log.debug("Got groups: %s" % groups)
+ checked_groups = []
+ for a,v in groups.items():
+ v = None if len(v) == 0 else v
+ safe = _check_option(a, v)
+ if safe is not None and not safe.strip() == "":
+ log.debug("Appending option: %s" % safe)
+ checked_groups.append(safe)
+ else:
+ log.warn("Dropped option: '%s %s'" % (a,v))
+ return checked_groups
+
+ if args is not None:
+ option_groups = {}
+ for arg in args:
+ ## if we're given a string with a bunch of options in it split
+ ## them up and deal with them separately
+ if (not _util._py3k and isinstance(arg, basestring)) \
+ or (_util._py3k and isinstance(arg, str)):
+ log.debug("Got arg string: %s" % arg)
+ if arg.find(' ') > 0:
+ filo = _make_filo(arg)
+ option_groups.update(_make_groups(filo))
+ else:
+ option_groups.update({ arg: "" })
+ elif isinstance(arg, list):
+ log.debug("Got arg list: %s" % arg)
+ arg.reverse()
+ option_groups.update(_make_groups(arg))
+ else:
+ log.warn("Got non-str/list arg: '%s', type '%s'"
+ % (arg, type(arg)))
+ checked = _check_groups(option_groups)
+ sanitised = ' '.join(x for x in checked)
+ return sanitised
+ else:
+ log.debug("Got None for args")
+
+def _sanitise_list(arg_list):
+ """A generator for iterating through a list of gpg options and sanitising
+ them.
+
+ :param list arg_list: A list of options and flags for GnuPG.
+ :rtype: generator
+ :returns: A generator whose next() method returns each of the items in
+ ``arg_list`` after calling ``_sanitise()`` with that item as a
+ parameter.
+ """
+ if isinstance(arg_list, list):
+ for arg in arg_list:
+ safe_arg = _sanitise(arg)
+ if safe_arg != "":
+ yield safe_arg
+
+def _get_options_group(group=None):
+ """Get a specific group of options which are allowed."""
+
+ #: These expect a hexidecimal keyid as their argument, and can be parsed
+ #: with :func:`_is_hex`.
+ hex_options = frozenset(['--check-sigs',
+ '--default-key',
+ '--default-recipient',
+ '--delete-keys',
+ '--delete-secret-keys',
+ '--delete-secret-and-public-keys',
+ '--desig-revoke',
+ '--export',
+ '--export-secret-keys',
+ '--export-secret-subkeys',
+ '--fingerprint',
+ '--gen-revoke',
+ '--list-key',
+ '--list-keys',
+ '--list-public-keys',
+ '--list-secret-keys',
+ '--list-sigs',
+ '--recipient',
+ '--recv-keys',
+ '--send-keys',
+ ])
+ #: These options expect value which are left unchecked, though still run
+ #: through :func:`_fix_unsafe`.
+ unchecked_options = frozenset(['--list-options',
+ '--passphrase-fd',
+ '--status-fd',
+ '--verify-options',
+ ])
+ #: These have their own parsers and don't really fit into a group
+ other_options = frozenset(['--debug-level',
+ '--keyserver',
+
+ ])
+ #: These should have a directory for an argument
+ dir_options = frozenset(['--homedir',
+ ])
+ #: These expect a keyring or keyfile as their argument
+ keyring_options = frozenset(['--keyring',
+ '--primary-keyring',
+ '--secret-keyring',
+ '--trustdb-name',
+ ])
+ #: These expect a filename (or the contents of a file as a string) or None
+ #: (meaning that they read from stdin)
+ file_or_none_options = frozenset(['--decrypt',
+ '--decrypt-files',
+ '--encrypt',
+ '--encrypt-files',
+ '--import',
+ '--verify',
+ '--verify-files',
+ ])
+ #: These options expect a string. see :func:`_check_preferences`.
+ pref_options = frozenset(['--digest-algo',
+ '--cipher-algo',
+ '--compress-algo',
+ '--compression-algo',
+ '--cert-digest-algo',
+ '--personal-digest-prefs',
+ '--personal-digest-preferences',
+ '--personal-cipher-prefs',
+ '--personal-cipher-preferences',
+ '--personal-compress-prefs',
+ '--personal-compress-preferences',
+ '--print-md',
+ ])
+ #: These options expect no arguments
+ none_options = frozenset(['--always-trust',
+ '--armor',
+ '--armour',
+ '--batch',
+ '--check-sigs',
+ '--check-trustdb',
+ '--clearsign',
+ '--debug-all',
+ '--default-recipient-self',
+ '--detach-sign',
+ '--export',
+ '--export-ownertrust',
+ '--export-secret-keys',
+ '--export-secret-subkeys',
+ '--fingerprint',
+ '--fixed-list-mode',
+ '--gen-key',
+ '--import-ownertrust',
+ '--list-config',
+ '--list-key',
+ '--list-keys',
+ '--list-packets',
+ '--list-public-keys',
+ '--list-secret-keys',
+ '--list-sigs',
+ '--no-default-keyring',
+ '--no-default-recipient',
+ '--no-emit-version',
+ '--no-options',
+ '--no-tty',
+ '--no-use-agent',
+ '--no-verbose',
+ '--print-mds',
+ '--quiet',
+ '--sign',
+ '--symmetric',
+ '--use-agent',
+ '--verbose',
+ '--version',
+ '--with-colons',
+ '--yes',
+ ])
+ #: These options expect either None or a hex string
+ hex_or_none_options = hex_options.intersection(none_options)
+ allowed = hex_options.union(unchecked_options, other_options, dir_options,
+ keyring_options, file_or_none_options,
+ pref_options, none_options)
+
+ if group and group in locals().keys():
+ return locals()[group]
+
+def _get_all_gnupg_options():
+ """Get all GnuPG options and flags.
+
+ This is hardcoded within a local scope to reduce the chance of a tampered
+ GnuPG binary reporting falsified option sets, i.e. because certain options
+ (namedly the ``--no-options`` option, which prevents the usage of gpg.conf
+ files) are necessary and statically specified in
+ :meth:`gnupg._meta.GPGBase._make_args`, if the inputs into Python are
+ already controlled, and we were to summon the GnuPG binary to ask it for
+ its options, it would be possible to receive a falsified options set
+ missing the ``--no-options`` option in response. This seems unlikely, and
+ the method is stupid and ugly, but at least we'll never have to debug
+ whether or not an option *actually* disappeared in a different GnuPG
+ version, or some funny business is happening.
+
+ These are the options as of GnuPG 1.4.12; the current stable branch of the
+ 2.1.x tree contains a few more -- if you need them you'll have to add them
+ in here.
+
+ :type gnupg_options: frozenset
+ :ivar gnupg_options: All known GPG options and flags.
+ :rtype: frozenset
+ :returns: ``gnupg_options``
+ """
+ three_hundred_eighteen = ("""
+--allow-freeform-uid --multifile
+--allow-multiple-messages --no
+--allow-multisig-verification --no-allow-freeform-uid
+--allow-non-selfsigned-uid --no-allow-multiple-messages
+--allow-secret-key-import --no-allow-non-selfsigned-uid
+--always-trust --no-armor
+--armor --no-armour
+--armour --no-ask-cert-expire
+--ask-cert-expire --no-ask-cert-level
+--ask-cert-level --no-ask-sig-expire
+--ask-sig-expire --no-auto-check-trustdb
+--attribute-fd --no-auto-key-locate
+--attribute-file --no-auto-key-retrieve
+--auto-check-trustdb --no-batch
+--auto-key-locate --no-comments
+--auto-key-retrieve --no-default-keyring
+--batch --no-default-recipient
+--bzip2-compress-level --no-disable-mdc
+--bzip2-decompress-lowmem --no-emit-version
+--card-edit --no-encrypt-to
+--card-status --no-escape-from-lines
+--cert-digest-algo --no-expensive-trust-checks
+--cert-notation --no-expert
+--cert-policy-url --no-force-mdc
+--change-pin --no-force-v3-sigs
+--charset --no-force-v4-certs
+--check-sig --no-for-your-eyes-only
+--check-sigs --no-greeting
+--check-trustdb --no-groups
+--cipher-algo --no-literal
+--clearsign --no-mangle-dos-filenames
+--command-fd --no-mdc-warning
+--command-file --no-options
+--comment --no-permission-warning
+--completes-needed --no-pgp2
+--compress-algo --no-pgp6
+--compression-algo --no-pgp7
+--compress-keys --no-pgp8
+--compress-level --no-random-seed-file
+--compress-sigs --no-require-backsigs
+--ctapi-driver --no-require-cross-certification
+--dearmor --no-require-secmem
+--dearmour --no-rfc2440-text
+--debug --no-secmem-warning
+--debug-all --no-show-notation
+--debug-ccid-driver --no-show-photos
+--debug-level --no-show-policy-url
+--decrypt --no-sig-cache
+--decrypt-files --no-sig-create-check
+--default-cert-check-level --no-sk-comments
+--default-cert-expire --no-strict
+--default-cert-level --notation-data
+--default-comment --not-dash-escaped
+--default-key --no-textmode
+--default-keyserver-url --no-throw-keyid
+--default-preference-list --no-throw-keyids
+--default-recipient --no-tty
+--default-recipient-self --no-use-agent
+--default-sig-expire --no-use-embedded-filename
+--delete-keys --no-utf8-strings
+--delete-secret-and-public-keys --no-verbose
+--delete-secret-keys --no-version
+--desig-revoke --openpgp
+--detach-sign --options
+--digest-algo --output
+--disable-ccid --override-session-key
+--disable-cipher-algo --passphrase
+--disable-dsa2 --passphrase-fd
+--disable-mdc --passphrase-file
+--disable-pubkey-algo --passphrase-repeat
+--display --pcsc-driver
+--display-charset --personal-cipher-preferences
+--dry-run --personal-cipher-prefs
+--dump-options --personal-compress-preferences
+--edit-key --personal-compress-prefs
+--emit-version --personal-digest-preferences
+--enable-dsa2 --personal-digest-prefs
+--enable-progress-filter --pgp2
+--enable-special-filenames --pgp6
+--enarmor --pgp7
+--enarmour --pgp8
+--encrypt --photo-viewer
+--encrypt-files --pipemode
+--encrypt-to --preserve-permissions
+--escape-from-lines --primary-keyring
+--exec-path --print-md
+--exit-on-status-write-error --print-mds
+--expert --quick-random
+--export --quiet
+--export-options --reader-port
+--export-ownertrust --rebuild-keydb-caches
+--export-secret-keys --recipient
+--export-secret-subkeys --recv-keys
+--fast-import --refresh-keys
+--fast-list-mode --remote-user
+--fetch-keys --require-backsigs
+--fingerprint --require-cross-certification
+--fixed-list-mode --require-secmem
+--fix-trustdb --rfc1991
+--force-mdc --rfc2440
+--force-ownertrust --rfc2440-text
+--force-v3-sigs --rfc4880
+--force-v4-certs --run-as-shm-coprocess
+--for-your-eyes-only --s2k-cipher-algo
+--gen-key --s2k-count
+--gen-prime --s2k-digest-algo
+--gen-random --s2k-mode
+--gen-revoke --search-keys
+--gnupg --secret-keyring
+--gpg-agent-info --send-keys
+--gpgconf-list --set-filename
+--gpgconf-test --set-filesize
+--group --set-notation
+--help --set-policy-url
+--hidden-encrypt-to --show-keyring
+--hidden-recipient --show-notation
+--homedir --show-photos
+--honor-http-proxy --show-policy-url
+--ignore-crc-error --show-session-key
+--ignore-mdc-error --sig-keyserver-url
+--ignore-time-conflict --sign
+--ignore-valid-from --sign-key
+--import --sig-notation
+--import-options --sign-with
+--import-ownertrust --sig-policy-url
+--interactive --simple-sk-checksum
+--keyid-format --sk-comments
+--keyring --skip-verify
+--keyserver --status-fd
+--keyserver-options --status-file
+--lc-ctype --store
+--lc-messages --strict
+--limit-card-insert-tries --symmetric
+--list-config --temp-directory
+--list-key --textmode
+--list-keys --throw-keyid
+--list-only --throw-keyids
+--list-options --trustdb-name
+--list-ownertrust --trusted-key
+--list-packets --trust-model
+--list-public-keys --try-all-secrets
+--list-secret-keys --ttyname
+--list-sig --ttytype
+--list-sigs --ungroup
+--list-trustdb --update-trustdb
+--load-extension --use-agent
+--local-user --use-embedded-filename
+--lock-multiple --user
+--lock-never --utf8-strings
+--lock-once --verbose
+--logger-fd --verify
+--logger-file --verify-files
+--lsign-key --verify-options
+--mangle-dos-filenames --version
+--marginals-needed --warranty
+--max-cert-depth --with-colons
+--max-output --with-fingerprint
+--merge-only --with-key-data
+--min-cert-level --yes
+""").split()
+
+ # These are extra options which only exist for GnuPG>=2.0.0
+ three_hundred_eighteen.append('--export-ownertrust')
+ three_hundred_eighteen.append('--import-ownertrust')
+
+ gnupg_options = frozenset(three_hundred_eighteen)
+ return gnupg_options
+
+def nodata(status_code):
+ """Translate NODATA status codes from GnuPG to messages."""
+ lookup = {
+ '1': 'No armored data.',
+ '2': 'Expected a packet but did not find one.',
+ '3': 'Invalid packet found, this may indicate a non OpenPGP message.',
+ '4': 'Signature expected but not found.' }
+ for key, value in lookup.items():
+ if str(status_code) == key:
+ return value
+
+def progress(status_code):
+ """Translate PROGRESS status codes from GnuPG to messages."""
+ lookup = {
+ 'pk_dsa': 'DSA key generation',
+ 'pk_elg': 'Elgamal key generation',
+ 'primegen': 'Prime generation',
+ 'need_entropy': 'Waiting for new entropy in the RNG',
+ 'tick': 'Generic tick without any special meaning - still working.',
+ 'starting_agent': 'A gpg-agent was started.',
+ 'learncard': 'gpg-agent or gpgsm is learning the smartcard data.',
+ 'card_busy': 'A smartcard is still working.' }
+ for key, value in lookup.items():
+ if str(status_code) == key:
+ return value
+
+
+class GenKey(object):
+ """Handle status messages for key generation.
+
+ Calling the ``__str__()`` method of this class will return the generated
+ key's fingerprint, or a status string explaining the results.
+ """
+ def __init__(self, gpg):
+ self._gpg = gpg
+ ## this should get changed to something more useful, like 'key_type'
+ #: 'P':= primary, 'S':= subkey, 'B':= both
+ self.type = None
+ self.fingerprint = None
+ self.status = None
+ self.subkey_created = False
+ self.primary_created = False
+ #: This will store the key's public keyring filename, if
+ #: :meth:`~gnupg.GPG.gen_key_input` was called with
+ #: ``separate_keyring=True``.
+ self.keyring = None
+ #: This will store the key's secret keyring filename, if :
+ #: :meth:`~gnupg.GPG.gen_key_input` was called with
+ #: ``separate_keyring=True``.
+ self.secring = None
+
+ def __nonzero__(self):
+ if self.fingerprint: return True
+ return False
+ __bool__ = __nonzero__
+
+ def __str__(self):
+ if self.fingerprint:
+ return self.fingerprint
+ else:
+ if self.status is not None:
+ return self.status
+ else:
+ return False
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
+ """
+ if key in ("GOOD_PASSPHRASE"):
+ pass
+ elif key == "KEY_NOT_CREATED":
+ self.status = 'key not created'
+ elif key == "KEY_CREATED":
+ (self.type, self.fingerprint) = value.split()
+ self.status = 'key created'
+ elif key == "NODATA":
+ self.status = nodata(value)
+ elif key == "PROGRESS":
+ self.status = progress(value.split(' ', 1)[0])
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+ if self.type in ('B', 'P'):
+ self.primary_created = True
+ if self.type in ('B', 'S'):
+ self.subkey_created = True
+
+class DeleteResult(object):
+ """Handle status messages for --delete-keys and --delete-secret-keys"""
+ def __init__(self, gpg):
+ self._gpg = gpg
+ self.status = 'ok'
+
+ def __str__(self):
+ return self.status
+
+ problem_reason = { '1': 'No such key',
+ '2': 'Must delete secret key first',
+ '3': 'Ambigious specification', }
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
+ """
+ if key == "DELETE_PROBLEM":
+ self.status = self.problem_reason.get(value, "Unknown error: %r"
+ % value)
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+class Sign(object):
+ """Parse GnuPG status messages for signing operations.
+
+ :param gpg: An instance of :class:`gnupg.GPG`.
+ """
+
+ #: The type of signature created.
+ sig_type = None
+ #: The algorithm used to create the signature.
+ sig_algo = None
+ #: The hash algorithm used to create the signature.
+ sig_hash_also = None
+ #: The fingerprint of the signing keyid.
+ fingerprint = None
+ #: The timestamp on the signature.
+ timestamp = None
+ #: xxx fill me in
+ what = None
+
+ def __init__(self, gpg):
+ self._gpg = gpg
+
+ def __nonzero__(self):
+ """Override the determination for truthfulness evaluation.
+
+ :rtype: bool
+ :returns: True if we have a valid signature, False otherwise.
+ """
+ return self.fingerprint is not None
+ __bool__ = __nonzero__
+
+ def __str__(self):
+ return self.data.decode(self._gpg._encoding, self._gpg._decode_errors)
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :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
+ elif key == "SIG_CREATED":
+ (self.sig_type, self.sig_algo, self.sig_hash_algo,
+ self.what, self.timestamp, self.fingerprint) = value.split()
+ elif key == "KEYEXPIRED":
+ self.status = "skipped signing key, key expired"
+ if (value is not None) and (len(value) > 0):
+ self.status += " on {}".format(str(value))
+ elif key == "KEYREVOKED":
+ self.status = "skipped signing key, key revoked"
+ if (value is not None) and (len(value) > 0):
+ self.status += " on {}".format(str(value))
+ elif key == "NODATA":
+ self.status = nodata(value)
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+class ListKeys(list):
+ """Handle status messages for --list-keys.
+
+ Handles pub and uid (relating the latter to the former). Don't care about
+ the following attributes/status messages (from doc/DETAILS):
+
+ | crt = X.509 certificate
+ | crs = X.509 certificate and private key available
+ | ssb = secret subkey (secondary key)
+ | uat = user attribute (same as user id except for field 10).
+ | sig = signature
+ | rev = revocation signature
+ | pkd = public key data (special field format, see below)
+ | grp = reserved for gpgsm
+ | rvk = revocation key
+ """
+
+ def __init__(self, gpg):
+ super(ListKeys, self).__init__()
+ self._gpg = gpg
+ self.curkey = None
+ self.fingerprints = []
+ self.uids = []
+
+ def key(self, args):
+ vars = ("""
+ type trust length algo keyid date expires dummy ownertrust uid
+ """).split()
+ self.curkey = {}
+ for i in range(len(vars)):
+ self.curkey[vars[i]] = args[i]
+ self.curkey['uids'] = []
+ if self.curkey['uid']:
+ self.curkey['uids'].append(self.curkey['uid'])
+ del self.curkey['uid']
+ self.curkey['subkeys'] = []
+ self.append(self.curkey)
+
+ pub = sec = key
+
+ def fpr(self, args):
+ self.curkey['fingerprint'] = args[9]
+ self.fingerprints.append(args[9])
+
+ def uid(self, args):
+ uid = args[9]
+ uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid)
+ self.curkey['uids'].append(uid)
+ self.uids.append(uid)
+
+ def sub(self, args):
+ subkey = [args[4], args[11]]
+ self.curkey['subkeys'].append(subkey)
+
+ def _handle_status(self, key, value):
+ pass
+
+
+class ImportResult(object):
+ """Parse GnuPG status messages for key import operations.
+
+ :type gpg: :class:`gnupg.GPG`
+ :param gpg: An instance of :class:`gnupg.GPG`.
+ """
+ _ok_reason = {'0': 'Not actually changed',
+ '1': 'Entirely new key',
+ '2': 'New user IDs',
+ '4': 'New signatures',
+ '8': 'New subkeys',
+ '16': 'Contains private key',
+ '17': 'Contains private key',}
+
+ _problem_reason = { '0': 'No specific reason given',
+ '1': 'Invalid Certificate',
+ '2': 'Issuer Certificate missing',
+ '3': 'Certificate Chain too long',
+ '4': 'Error storing certificate', }
+
+ _fields = '''count no_user_id imported imported_rsa unchanged
+ n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups
+ not_imported'''.split()
+ _counts = OrderedDict(
+ zip(_fields, [int(0) for x in range(len(_fields))]) )
+
+ #: A list of strings containing the fingerprints of the GnuPG keyIDs
+ #: imported.
+ fingerprints = list()
+
+ #: A list containing dictionaries with information gathered on keys
+ #: imported.
+ results = list()
+
+ def __init__(self, gpg):
+ self._gpg = gpg
+ self.counts = self._counts
+
+ def __nonzero__(self):
+ """Override the determination for truthfulness evaluation.
+
+ :rtype: bool
+ :returns: True if we have immport some keys, False otherwise.
+ """
+ if self.counts.not_imported > 0: return False
+ if len(self.fingerprints) == 0: return False
+ return True
+ __bool__ = __nonzero__
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
+ """
+ if key == "IMPORTED":
+ # this duplicates info we already see in import_ok & import_problem
+ pass
+ elif key == "NODATA":
+ self.results.append({'fingerprint': None,
+ 'status': 'No valid data found'})
+ elif key == "IMPORT_OK":
+ reason, fingerprint = value.split()
+ reasons = []
+ for code, text in self._ok_reason.items():
+ if int(reason) == int(code):
+ reasons.append(text)
+ reasontext = '\n'.join(reasons) + "\n"
+ self.results.append({'fingerprint': fingerprint,
+ 'status': reasontext})
+ self.fingerprints.append(fingerprint)
+ elif key == "IMPORT_PROBLEM":
+ try:
+ reason, fingerprint = value.split()
+ except:
+ reason = value
+ fingerprint = '<unknown>'
+ self.results.append({'fingerprint': fingerprint,
+ 'status': self._problem_reason[reason]})
+ elif key == "IMPORT_RES":
+ import_res = value.split()
+ for x in self.counts.keys():
+ self.counts[x] = int(import_res.pop(0))
+ elif key == "KEYEXPIRED":
+ res = {'fingerprint': None,
+ 'status': 'Key expired'}
+ self.results.append(res)
+ ## Accoring to docs/DETAILS L859, SIGEXPIRED is obsolete:
+ ## "Removed on 2011-02-04. This is deprecated in favor of KEYEXPIRED."
+ elif key == "SIGEXPIRED":
+ res = {'fingerprint': None,
+ 'status': 'Signature expired'}
+ self.results.append(res)
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+ def summary(self):
+ l = []
+ l.append('%d imported' % self.counts['imported'])
+ if self.counts['not_imported']:
+ l.append('%d not imported' % self.counts['not_imported'])
+ return ', '.join(l)
+
+
+class Verify(object):
+ """Parser for status messages from GnuPG for certifications and signature
+ verifications.
+
+ People often mix these up, or think that they are the same thing. While it
+ is true that certifications and signatures *are* the same cryptographic
+ operation -- and also true that both are the same as the decryption
+ operation -- a distinction is made for important reasons.
+
+ A certification:
+ * is made on a key,
+ * can help to validate or invalidate the key owner's identity,
+ * can assign trust levels to the key (or to uids and/or subkeys that
+ the key contains),
+ * and can be used in absense of in-person fingerprint checking to try
+ to build a path (through keys whose fingerprints have been checked)
+ to the key, so that the identity of the key's owner can be more
+ reliable without having to actually physically meet in person.
+
+ A signature:
+ * is created for a file or other piece of data,
+ * can help to prove that the data hasn't been altered,
+ * and can help to prove that the data was sent by the person(s) in
+ possession of the private key that created the signature, and for
+ parsing portions of status messages from decryption operations.
+
+ There are probably other things unique to each that have been
+ scatterbrainedly omitted due to the programmer sitting still and staring
+ at GnuPG debugging logs for too long without snacks, but that is the gist
+ of it.
+ """
+
+ TRUST_UNDEFINED = 0
+ TRUST_NEVER = 1
+ TRUST_MARGINAL = 2
+ TRUST_FULLY = 3
+ TRUST_ULTIMATE = 4
+
+ TRUST_LEVELS = {"TRUST_UNDEFINED" : TRUST_UNDEFINED,
+ "TRUST_NEVER" : TRUST_NEVER,
+ "TRUST_MARGINAL" : TRUST_MARGINAL,
+ "TRUST_FULLY" : TRUST_FULLY,
+ "TRUST_ULTIMATE" : TRUST_ULTIMATE,}
+
+ def __init__(self, gpg):
+ """Create a parser for verification and certification commands.
+
+ :param gpg: An instance of :class:`gnupg.GPG`.
+ """
+ self._gpg = gpg
+ #: True if the signature is valid, False otherwise.
+ self.valid = False
+ #: A string describing the status of the signature verification.
+ #: Can be one of ``signature bad``, ``signature good``,
+ #: ``signature valid``, ``signature error``, ``decryption failed``,
+ #: ``no public key``, ``key exp``, or ``key rev``.
+ self.status = None
+ #: The fingerprint of the signing keyid.
+ self.fingerprint = None
+ #: The fingerprint of the corresponding public key, which may be
+ #: different if the signature was created with a subkey.
+ self.pubkey_fingerprint = None
+ #: The keyid of the signing key.
+ self.key_id = None
+ #: The id of the signature itself.
+ self.signature_id = None
+ #: The creation date of the signing key.
+ self.creation_date = None
+ #: The timestamp of the purported signature, if we are unable to parse
+ #: and/or validate it.
+ self.timestamp = None
+ #: The timestamp for when the valid signature was created.
+ self.sig_timestamp = None
+ #: The userid of the signing key which was used to create the
+ #: signature.
+ self.username = None
+ #: When the signing key is due to expire.
+ self.expire_timestamp = None
+ #: An integer 0-4 describing the trust level of the signature.
+ self.trust_level = None
+ #: The string corresponding to the ``trust_level`` number.
+ self.trust_text = None
+
+ def __nonzero__(self):
+ """Override the determination for truthfulness evaluation.
+
+ :rtype: bool
+ :returns: True if we have a valid signature, False otherwise.
+ """
+ return self.valid
+ __bool__ = __nonzero__
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
+ """
+ if key in self.TRUST_LEVELS:
+ self.trust_text = key
+ self.trust_level = self.TRUST_LEVELS[key]
+ elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT",
+ "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO",
+ "DECRYPTION_OKAY", "INV_SGNR"):
+ pass
+ elif key == "BADSIG":
+ self.valid = False
+ self.status = 'signature bad'
+ self.key_id, self.username = value.split(None, 1)
+ elif key == "GOODSIG":
+ self.valid = True
+ self.status = 'signature good'
+ self.key_id, self.username = value.split(None, 1)
+ elif key == "VALIDSIG":
+ (self.fingerprint,
+ self.creation_date,
+ self.sig_timestamp,
+ self.expire_timestamp) = value.split()[:4]
+ # may be different if signature is made with a subkey
+ self.pubkey_fingerprint = value.split()[-1]
+ self.status = 'signature valid'
+ elif key == "SIG_ID":
+ (self.signature_id,
+ self.creation_date, self.timestamp) = value.split()
+ elif key == "ERRSIG":
+ self.valid = False
+ (self.key_id,
+ algo, hash_algo,
+ cls,
+ self.timestamp) = value.split()[:5]
+ self.status = 'signature error'
+ elif key == "DECRYPTION_FAILED":
+ self.valid = False
+ self.key_id = value
+ self.status = 'decryption failed'
+ elif key == "NO_PUBKEY":
+ self.valid = False
+ self.key_id = value
+ self.status = 'no public key'
+ elif key in ("KEYEXPIRED", "SIGEXPIRED"):
+ # these are useless in verify, since they are spit out for any
+ # pub/subkeys on the key, not just the one doing the signing.
+ # if we want to check for signatures with expired key,
+ # the relevant flag is EXPKEYSIG.
+ pass
+ elif key in ("EXPKEYSIG", "REVKEYSIG"):
+ # signed with expired or revoked key
+ self.valid = False
+ self.key_id = value.split()[0]
+ self.status = (('%s %s') % (key[:3], key[3:])).lower()
+ else:
+ raise ValueError("Unknown status message: %r" % key)
+
+
+class Crypt(Verify):
+ """Parser for internal status messages from GnuPG for ``--encrypt``,
+ ``--decrypt``, and ``--decrypt-files``.
+ """
+ def __init__(self, gpg):
+ Verify.__init__(self, gpg)
+ self._gpg = gpg
+ #: A string containing the encrypted or decrypted data.
+ self.data = ''
+ #: True if the decryption/encryption process turned out okay.
+ self.ok = False
+ #: A string describing the current processing status, or error, if one
+ #: has occurred.
+ self.status = None
+ self.data_format = None
+ self.data_timestamp = None
+ self.data_filename = None
+
+ def __nonzero__(self):
+ if self.ok: return True
+ return False
+ __bool__ = __nonzero__
+
+ def __str__(self):
+ """The str() method for a :class:`Crypt` object will automatically return the
+ decoded data string, which stores the encryped or decrypted data.
+
+ In other words, these two statements are equivalent:
+
+ >>> assert decrypted.data == str(decrypted)
+
+ """
+ return self.data.decode(self._gpg._encoding, self._gpg._decode_errors)
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
+ """
+ if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION",
+ "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA",
+ "CARDCTRL"):
+ # in the case of ERROR, this is because a more specific error
+ # message will have come first
+ pass
+ elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE",
+ "MISSING_PASSPHRASE", "DECRYPTION_FAILED",
+ "KEY_NOT_CREATED"):
+ self.status = key.replace("_", " ").lower()
+ elif key == "NEED_TRUSTDB":
+ self._gpg._create_trustdb()
+ elif key == "NEED_PASSPHRASE_SYM":
+ self.status = 'need symmetric passphrase'
+ elif key == "BEGIN_DECRYPTION":
+ self.status = 'decryption incomplete'
+ elif key == "BEGIN_ENCRYPTION":
+ self.status = 'encryption incomplete'
+ elif key == "DECRYPTION_OKAY":
+ self.status = 'decryption ok'
+ self.ok = True
+ elif key == "END_ENCRYPTION":
+ self.status = 'encryption ok'
+ self.ok = True
+ elif key == "INV_RECP":
+ self.status = 'invalid recipient'
+ elif key == "KEYEXPIRED":
+ self.status = 'key expired'
+ elif key == "KEYREVOKED":
+ self.status = 'key revoked'
+ elif key == "SIG_CREATED":
+ self.status = 'sig created'
+ elif key == "SIGEXPIRED":
+ self.status = 'sig expired'
+ elif key == "PLAINTEXT":
+ fmt, dts = value.split(' ', 1)
+ if dts.find(' ') > 0:
+ self.data_timestamp, self.data_filename = dts.split(' ', 1)
+ else:
+ self.data_timestamp = dts
+ ## GnuPG gives us a hex byte for an ascii char corresponding to
+ ## the data format of the resulting plaintext,
+ ## i.e. '62'→'b':= binary data
+ self.data_format = chr(int(str(fmt), 16))
+ else:
+ super(Crypt, self)._handle_status(key, value)
+
+class ListPackets(object):
+ """Handle status messages for --list-packets."""
+
+ def __init__(self, gpg):
+ self._gpg = gpg
+ #: A string describing the current processing status, or error, if one
+ #: has occurred.
+ self.status = None
+ #: True if the passphrase to a public/private keypair is required.
+ self.need_passphrase = None
+ #: True if a passphrase for a symmetric key is required.
+ self.need_passphrase_sym = None
+ #: The keyid and uid which this data is encrypted to.
+ self.userid_hint = None
+
+ def _handle_status(self, key, value):
+ """Parse a status code from the attached GnuPG process.
+
+ :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
+ """
+ if key == 'NODATA':
+ self.status = nodata(value)
+ elif key == 'ENC_TO':
+ # This will only capture keys in our keyring. In the future we
+ # may want to include multiple unknown keys in this list.
+ self.key, _, _ = value.split()
+ elif key == 'NEED_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)
diff --git a/gnupg/_trust.py b/gnupg/_trust.py
new file mode 100644
index 0000000..514ae8c
--- /dev/null
+++ b/gnupg/_trust.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+'''Functions for handling trustdb and trust calculations.
+
+The functions within this module take an instance of :class:`gnupg.GPGBase` or
+a suitable subclass as their first argument.
+'''
+
+from __future__ import absolute_import
+
+import os
+
+from . import _util
+from ._util import log
+
+def _create_trustdb(cls):
+ """Create the trustdb file in our homedir, if it doesn't exist."""
+ trustdb = os.path.join(cls.homedir, 'trustdb.gpg')
+ if not os.path.isfile(trustdb):
+ log.info("GnuPG complained that your trustdb file was missing. %s"
+ % "This is likely due to changing to a new homedir.")
+ log.info("Creating trustdb.gpg file in your GnuPG homedir.")
+ cls.fix_trustdb(trustdb)
+
+def export_ownertrust(cls, trustdb=None):
+ """Export ownertrust to a trustdb file.
+
+ If there is already a file named :file:`trustdb.gpg` in the current GnuPG
+ homedir, it will be renamed to :file:`trustdb.gpg.bak`.
+
+ :param string trustdb: The path to the trustdb.gpg file. If not given,
+ defaults to ``'trustdb.gpg'`` in the current GnuPG
+ homedir.
+ """
+ if trustdb is None:
+ trustdb = os.path.join(cls.homedir, 'trustdb.gpg')
+
+ try:
+ os.rename(trustdb, trustdb + '.bak')
+ except (OSError, IOError) as err:
+ log.debug(str(err))
+
+ export_proc = cls._open_subprocess('--export-ownertrust')
+ tdb = open(trustdb, 'wb')
+ _util._threaded_copy_data(export_proc.stdout, tdb)
+
+def import_ownertrust(self, trustdb=None):
+ """Import ownertrust from a trustdb file.
+
+ :param str trustdb: The path to the trustdb.gpg file. If not given,
+ defaults to :file:`trustdb.gpg` in the current GnuPG
+ homedir.
+ """
+ if trustdb is None:
+ trustdb = os.path.join(cls.homedir, 'trustdb.gpg')
+
+ import_proc = cls._open_subprocess('--import-ownertrust')
+ tdb = open(trustdb, 'rb')
+ _util._threaded_copy_data(tdb, import_proc.stdin)
+
+def fix_trustdb(cls, trustdb=None):
+ """Attempt to repair a broken trustdb.gpg file.
+
+ GnuPG>=2.0.x has this magical-seeming flag: `--fix-trustdb`. You'd think
+ it would fix the the trustdb. Hah! It doesn't. Here's what it does
+ instead::
+
+ (gpg)~/code/python-gnupg $ gpg2 --fix-trustdb
+ gpg: You may try to re-create the trustdb using the commands:
+ gpg: cd ~/.gnupg
+ gpg: gpg2 --export-ownertrust > otrust.tmp
+ gpg: rm trustdb.gpg
+ gpg: gpg2 --import-ownertrust < otrust.tmp
+ gpg: If that does not work, please consult the manual
+
+ Brilliant piece of software engineering right there.
+
+ :param str trustdb: The path to the trustdb.gpg file. If not given,
+ defaults to :file:`trustdb.gpg` in the current GnuPG
+ homedir.
+ """
+ if trustdb is None:
+ trustdb = os.path.join(cls.homedir, 'trustdb.gpg')
+ export_proc = cls._open_subprocess('--export-ownertrust')
+ import_proc = cls._open_subprocess('--import-ownertrust')
+ _util._threaded_copy_data(export_proc.stdout, import_proc.stdin)
diff --git a/gnupg/_util.py b/gnupg/_util.py
new file mode 100644
index 0000000..e1e14ab
--- /dev/null
+++ b/gnupg/_util.py
@@ -0,0 +1,617 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+'''Extra utilities for python-gnupg.'''
+
+from __future__ import absolute_import
+from datetime import datetime
+from socket import gethostname
+from time import localtime
+from time import mktime
+
+import codecs
+import encodings
+import os
+import psutil
+import threading
+import random
+import re
+import string
+import sys
+
+try:
+ from io import StringIO
+ from io import BytesIO
+except ImportError:
+ from cStringIO import StringIO
+
+from . import _logger
+
+
+try:
+ unicode
+ _py3k = False
+ try:
+ isinstance(__name__, basestring)
+ except NameError:
+ msg = "Sorry, python-gnupg requires a Python version with proper"
+ msg += " unicode support. Please upgrade to Python>=2.6."
+ raise SystemExit(msg)
+except NameError:
+ _py3k = True
+
+
+## Directory shortcuts:
+## we don't want to use this one because it writes to the install dir:
+#_here = getabsfile(currentframe()).rsplit(os.path.sep, 1)[0]
+_here = os.path.join(os.getcwd(), 'gnupg') ## current dir
+_test = os.path.join(os.path.join(_here, 'test'), 'tmp') ## ./tests/tmp
+_user = os.environ.get('HOME') ## $HOME
+_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg
+_conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg')
+ ## $HOME/.config/python-gnupg
+
+## Logger is disabled by default
+log = _logger.create_logger(0)
+
+
+def find_encodings(enc=None, system=False):
+ """Find functions for encoding translations for a specific codec.
+
+ :param str enc: The codec to find translation functions for. It will be
+ normalized by converting to lowercase, excluding
+ everything which is not ascii, and hyphens will be
+ converted to underscores.
+
+ :param bool system: If True, find encodings based on the system's stdin
+ encoding, otherwise assume utf-8.
+
+ :raises: :exc:LookupError if the normalized codec, ``enc``, cannot be
+ found in Python's encoding translation map.
+ """
+ if not enc:
+ enc = 'utf-8'
+
+ if system:
+ if getattr(sys.stdin, 'encoding', None) is None:
+ enc = sys.stdin.encoding
+ log.debug("Obtained encoding from stdin: %s" % enc)
+ else:
+ enc = 'ascii'
+
+ ## have to have lowercase to work, see
+ ## http://docs.python.org/dev/library/codecs.html#standard-encodings
+ enc = enc.lower()
+ codec_alias = encodings.normalize_encoding(enc)
+
+ codecs.register(encodings.search_function)
+ coder = codecs.lookup(codec_alias)
+
+ return coder
+
+def author_info(name, contact=None, public_key=None):
+ """Easy object-oriented representation of contributor info.
+
+ :param str name: The contributor´s name.
+ :param str contact: The contributor´s email address or contact
+ information, if given.
+ :param str public_key: The contributor´s public keyid, if given.
+ """
+ return Storage(name=name, contact=contact, public_key=public_key)
+
+def _copy_data(instream, outstream):
+ """Copy data from one stream to another.
+
+ :type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file
+ :param instream: A byte stream or open file to read from.
+ :param file outstream: The file descriptor of a tmpfile to write to.
+ """
+ sent = 0
+
+ coder = find_encodings()
+
+ while True:
+ if ((_py3k and isinstance(instream, str)) or
+ (not _py3k and isinstance(instream, basestring))):
+ data = instream[:1024]
+ instream = instream[1024:]
+ else:
+ 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:
+ try:
+ outstream.write(coder.encode(data))
+ except IOError:
+ log.exception("Error sending data: Broken pipe")
+ 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
+ try:
+ outstream.close()
+ except IOError as ioe:
+ log.error("Unable to close outstream %s:\r\t%s" % (outstream, ioe))
+ else:
+ log.debug("Closed outstream: %d bytes sent." % sent)
+
+def _create_if_necessary(directory):
+ """Create the specified directory, if necessary.
+
+ :param str directory: The directory to use.
+ :rtype: bool
+ :returns: True if no errors occurred and the directory was created or
+ existed beforehand, False otherwise.
+ """
+
+ if not os.path.isabs(directory):
+ log.debug("Got non-absolute path: %s" % directory)
+ directory = os.path.abspath(directory)
+
+ if not os.path.isdir(directory):
+ log.info("Creating directory: %s" % directory)
+ try:
+ os.makedirs(directory, 0x1C0)
+ except OSError as ose:
+ log.error(ose, exc_info=1)
+ return False
+ else:
+ log.debug("Created directory.")
+ return True
+
+def create_uid_email(username=None, hostname=None):
+ """Create an email address suitable for a UID on a GnuPG key.
+
+ :param str username: The username portion of an email address. If None,
+ defaults to the username of the running Python
+ process.
+
+ :param str hostname: The FQDN portion of an email address. If None, the
+ hostname is obtained from gethostname(2).
+
+ :rtype: str
+ :returns: A string formatted as <username>@<hostname>.
+ """
+ if hostname:
+ hostname = hostname.replace(' ', '_')
+ if not username:
+ try: username = os.environ['LOGNAME']
+ except KeyError: username = os.environ['USERNAME']
+
+ if not hostname: hostname = gethostname()
+
+ uid = "%s@%s" % (username.replace(' ', '_'), hostname)
+ else:
+ username = username.replace(' ', '_')
+ if (not hostname) and (username.find('@') == 0):
+ uid = "%s@%s" % (username, gethostname())
+ elif hostname:
+ uid = "%s@%s" % (username, hostname)
+ else:
+ uid = username
+
+ return uid
+
+def _deprefix(line, prefix, callback=None):
+ """Remove the prefix string from the beginning of line, if it exists.
+
+ :param string line: A line, such as one output by GnuPG's status-fd.
+ :param string prefix: A substring to remove from the beginning of
+ ``line``. Case insensitive.
+ :type callback: callable
+ :param callback: Function to call if the prefix is found. The signature to
+ callback will be only one argument, the ``line`` without the ``prefix``, i.e.
+ ``callback(line)``.
+ :rtype: string
+ :returns: If the prefix was found, the ``line`` without the prefix is
+ returned. Otherwise, the original ``line`` is returned.
+ """
+ try:
+ assert line.upper().startswith(u''.join(prefix).upper())
+ except AssertionError:
+ log.debug("Line doesn't start with prefix '%s':\n%s" % (prefix, line))
+ return line
+ else:
+ newline = line[len(prefix):]
+ if callback is not None:
+ try:
+ callback(newline)
+ except Exception as exc:
+ log.exception(exc)
+ return newline
+
+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 binary: The path to the GnuPG binary.
+ :raises: :exc:`~exceptions.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.
+ """
+ found = None
+ if binary is not None:
+ if not os.path.isabs(binary):
+ try:
+ found = _which(binary)
+ log.debug("Found potential binary paths: %s"
+ % '\n'.join([path for path in found]))
+ found = found[0]
+ except IndexError as ie:
+ log.info("Could not determine absolute path of binary: '%s'"
+ % binary)
+ elif os.access(binary, os.X_OK):
+ found = binary
+ if found is None:
+ try: found = _which('gpg')[0]
+ except IndexError as ie:
+ log.error("Could not find binary for 'gpg'.")
+ try: found = _which('gpg2')[0]
+ except IndexError as ie:
+ log.error("Could not find binary for 'gpg2'.")
+ if found is None:
+ raise RuntimeError("GnuPG is not installed!")
+
+ try:
+ assert os.path.isabs(found), "Path to gpg binary not absolute"
+ assert not os.path.islink(found), "Path to gpg binary is symlink"
+ assert os.access(found, os.X_OK), "Lacking +x perms for gpg binary"
+ except (AssertionError, AttributeError) as ae:
+ log.error(str(ae))
+ else:
+ return found
+
+def _has_readwrite(path):
+ """
+ Determine if the real uid/gid of the executing user has read and write
+ permissions for a directory or a file.
+
+ :param str path: The path to the directory or file to check permissions
+ for.
+ :rtype: bool
+ :returns: True if real uid/gid has read+write permissions, False otherwise.
+ """
+ return os.access(path, os.R_OK ^ os.W_OK)
+
+def _is_file(filename):
+ """Check that the size of the thing which is supposed to be a filename has
+ size greater than zero, without following symbolic links or using
+ :func:os.path.isfile.
+
+ :param filename: An object to check.
+ :rtype: bool
+ :returns: True if **filename** is file-like, False otherwise.
+ """
+ try:
+ statinfo = os.lstat(filename)
+ log.debug("lstat(%r) with type=%s gave us %r"
+ % (repr(filename), type(filename), repr(statinfo)))
+ if not (statinfo.st_size > 0):
+ raise ValueError("'%s' appears to be an empty file!" % filename)
+ except OSError as oserr:
+ log.error(oserr)
+ if filename == '-':
+ log.debug("Got '-' for filename, assuming sys.stdin...")
+ return True
+ except (ValueError, TypeError, IOError) as err:
+ log.error(err)
+ else:
+ return True
+ return False
+
+def _is_stream(input):
+ """Check that the input is a byte stream.
+
+ :param input: An object provided for reading from or writing to.
+ :rtype: bool
+ :returns: True if :param:input is a stream, False if otherwise.
+ """
+ return isinstance(input, BytesIO) or isinstance(input, StringIO)
+
+def _is_list_or_tuple(instance):
+ """Check that ``instance`` is a list or tuple.
+
+ :param instance: The object to type check.
+ :rtype: bool
+ :returns: True if ``instance`` is a list or tuple, False otherwise.
+ """
+ return isinstance(instance, (list, tuple,))
+
+def _is_gpg1(version):
+ """Returns True if using GnuPG version 1.x.
+
+ :param tuple version: A tuple of three integers indication major, minor,
+ and micro version numbers.
+ """
+ (major, minor, micro) = _match_version_string(version)
+ if major == 1:
+ return True
+ return False
+
+def _is_gpg2(version):
+ """Returns True if using GnuPG version 2.x.
+
+ :param tuple version: A tuple of three integers indication major, minor,
+ and micro version numbers.
+ """
+ (major, minor, micro) = _match_version_string(version)
+ if major == 2:
+ return True
+ return False
+
+def _make_binary_stream(s, encoding):
+ """
+ xxx fill me in
+ """
+ try:
+ if _py3k:
+ if isinstance(s, str):
+ s = s.encode(encoding)
+ else:
+ if type(s) is not str:
+ s = s.encode(encoding)
+ from io import BytesIO
+ rv = BytesIO(s)
+ except ImportError:
+ rv = StringIO(s)
+ return rv
+
+def _make_passphrase(length=None, save=False, file=None):
+ """Create a passphrase and write it to a file that only the user can read.
+
+ This is not very secure, and should not be relied upon for actual key
+ passphrases.
+
+ :param int length: The length in bytes of the string to generate.
+
+ :param file file: The file to save the generated passphrase in. If not
+ given, defaults to 'passphrase-<the real user id>-<seconds since
+ epoch>' in the top-level directory.
+ """
+ if not length:
+ length = 40
+
+ passphrase = _make_random_string(length)
+
+ if save:
+ ruid, euid, suid = psutil.Process(os.getpid()).uids
+ gid = os.getgid()
+ now = mktime(localtime())
+
+ if not file:
+ filename = str('passphrase-%s-%s' % uid, now)
+ file = os.path.join(_repo, filename)
+
+ with open(file, 'a') as fh:
+ fh.write(passphrase)
+ fh.flush()
+ fh.close()
+ os.chmod(file, stat.S_IRUSR | stat.S_IWUSR)
+ os.chown(file, ruid, gid)
+
+ log.warn("Generated passphrase saved to %s" % file)
+ return passphrase
+
+def _make_random_string(length):
+ """Returns a random lowercase, uppercase, alphanumerical string.
+
+ :param int length: The length in bytes of the string to generate.
+ """
+ chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
+ return ''.join(random.choice(chars) for x in range(length))
+
+def _match_version_string(version):
+ """Sort a binary version string into major, minor, and micro integers.
+
+ :param str version: A version string in the form x.x.x
+ """
+ regex = re.compile('(\d)*(\.)*(\d)*(\.)*(\d)*')
+ matched = regex.match(version)
+ g = matched.groups()
+ major, minor, micro = int(g[0]), int(g[2]), int(g[4])
+ return (major, minor, micro)
+
+def _next_year():
+ """Get the date of today plus one year.
+
+ :rtype: str
+ :returns: The date of this day next year, in the format '%Y-%m-%d'.
+ """
+ now = datetime.now().__str__()
+ date = now.split(' ', 1)[0]
+ year, month, day = date.split('-', 2)
+ next_year = str(int(year)+1)
+ return '-'.join((next_year, month, day))
+
+def _now():
+ """Get a timestamp for right now, formatted according to ISO 8601."""
+ return datetime.isoformat(datetime.now())
+
+def _separate_keyword(line):
+ """Split the line, and return (first_word, the_rest)."""
+ try:
+ first, rest = line.split(None, 1)
+ except ValueError:
+ first = line.strip()
+ rest = ''
+ return first, rest
+
+def _threaded_copy_data(instream, outstream):
+ """Copy data from one stream to another in a separate thread.
+
+ Wraps ``_copy_data()`` in a :class:`threading.Thread`.
+
+ :type instream: :class:`io.BytesIO` or :class:`io.StringIO`
+ :param instream: A byte stream to read from.
+ :param file outstream: The file descriptor of a tmpfile to write to.
+ """
+ copy_thread = threading.Thread(target=_copy_data,
+ args=(instream, outstream))
+ copy_thread.setDaemon(True)
+ log.debug('%r, %r, %r', copy_thread, instream, outstream)
+ copy_thread.start()
+ return copy_thread
+
+def _utc_epoch():
+ """Get the seconds since epoch."""
+ return int(mktime(localtime()))
+
+def _which(executable, flags=os.X_OK):
+ """Borrowed from Twisted's :mod:twisted.python.proutils .
+
+ Search PATH for executable files with the given name.
+
+ On newer versions of MS-Windows, the PATHEXT environment variable will be
+ set to the list of file extensions for files considered executable. This
+ will normally include things like ".EXE". This fuction will also find files
+ with the given name ending with any of these extensions.
+
+ On MS-Windows the only flag that has any meaning is os.F_OK. Any other
+ flags will be ignored.
+
+ Note: This function does not help us prevent an attacker who can already
+ manipulate the environment's PATH settings from placing malicious code
+ higher in the PATH. It also does happily follows links.
+
+ :param str name: The name for which to search.
+ :param int flags: Arguments to L{os.access}.
+ :rtype: list
+ :returns: A list of the full paths to files found, in the order in which
+ they were found.
+ """
+ result = []
+ exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep))
+ path = os.environ.get('PATH', None)
+ if path is None:
+ return []
+ for p in os.environ.get('PATH', '').split(os.pathsep):
+ p = os.path.join(p, executable)
+ if os.access(p, flags):
+ result.append(p)
+ for e in exts:
+ pext = p + e
+ if os.access(pext, flags):
+ result.append(pext)
+ return result
+
+def _write_passphrase(stream, passphrase, encoding):
+ """Write the passphrase from memory to the GnuPG process' stdin.
+
+ :type stream: file, :class:`~io.BytesIO`, or :class:`~io.StringIO`
+ :param stream: The input file descriptor to write the password to.
+ :param str passphrase: The passphrase for the secret key material.
+ :param str encoding: The data encoding expected by GnuPG. Usually, this
+ is ``sys.getfilesystemencoding()``.
+ """
+ passphrase = '%s\n' % passphrase
+ passphrase = passphrase.encode(encoding)
+ stream.write(passphrase)
+ log.debug("Wrote passphrase on stdin.")
+
+
+class InheritableProperty(object):
+ """Based on the emulation of PyProperty_Type() in Objects/descrobject.c"""
+
+ def __init__(self, fget=None, fset=None, fdel=None, doc=None):
+ self.fget = fget
+ self.fset = fset
+ self.fdel = fdel
+ self.__doc__ = doc
+
+ def __get__(self, obj, objtype=None):
+ if obj is None:
+ return self
+ if self.fget is None:
+ raise AttributeError("unreadable attribute")
+ if self.fget.__name__ == '<lambda>' or not self.fget.__name__:
+ return self.fget(obj)
+ else:
+ return getattr(obj, self.fget.__name__)()
+
+ def __set__(self, obj, value):
+ if self.fset is None:
+ raise AttributeError("can't set attribute")
+ if self.fset.__name__ == '<lambda>' or not self.fset.__name__:
+ self.fset(obj, value)
+ else:
+ getattr(obj, self.fset.__name__)(value)
+
+ def __delete__(self, obj):
+ if self.fdel is None:
+ raise AttributeError("can't delete attribute")
+ if self.fdel.__name__ == '<lambda>' or not self.fdel.__name__:
+ self.fdel(obj)
+ else:
+ getattr(obj, self.fdel.__name__)()
+
+
+class Storage(dict):
+ """A dictionary where keys are stored as class attributes.
+
+ For example, ``obj.foo`` can be used in addition to ``obj['foo']``:
+
+ >>> o = Storage(a=1)
+ >>> o.a
+ 1
+ >>> o['a']
+ 1
+ >>> o.a = 2
+ >>> o['a']
+ 2
+ >>> del o.a
+ >>> o.a
+ None
+ """
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except KeyError as k:
+ return None
+
+ def __setattr__(self, key, value):
+ self[key] = value
+
+ def __delattr__(self, key):
+ try:
+ del self[key]
+ except KeyError as k:
+ raise AttributeError(k.args[0])
+
+ def __repr__(self):
+ return '<Storage ' + dict.__repr__(self) + '>'
+
+ def __getstate__(self):
+ return dict(self)
+
+ def __setstate__(self, value):
+ for (k, v) in value.items():
+ self[k] = v
diff --git a/gnupg/_version.py b/gnupg/_version.py
new file mode 100644
index 0000000..f9859c5
--- /dev/null
+++ b/gnupg/_version.py
@@ -0,0 +1,11 @@
+
+# This file was generated by 'versioneer.py' (0.7+) from
+# revision-control system data, or from the parent directory name of an
+# unpacked source archive. Distribution tarballs contain a pre-generated copy
+# of this file.
+
+version_version = '1.2.6-6-g6fa8b59'
+version_full = '6fa8b59d33ff573a988a8195bd513526feb81af2'
+def get_versions(default={}, verbose=False):
+ return {'version': version_version, 'full': version_full}
+
diff --git a/gnupg/copyleft.py b/gnupg/copyleft.py
new file mode 100644
index 0000000..6e81e1c
--- /dev/null
+++ b/gnupg/copyleft.py
@@ -0,0 +1,749 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+'''copyleft.py
+--------------
+Copyright information for python-gnupg.
+'''
+
+from __future__ import absolute_import
+
+from . import _util
+
+
+authors = { 'lovecruft_isis': _util.author_info(
+ 'Isis Agora Lovecruft', 'isis@leap.se', '0xA3ADB67A2CDB8B35'),
+
+ 'sajip_vinay': _util.author_info(
+ 'Vinay Sajip', 'vinay.sajip@gmail.com', '0xDE6EF0B2'),
+
+ 'traugott_steve': _util.author_info(
+ 'Steve Traugott', 'stevegt@terraluna.org'),
+
+ 'kuchling_am': _util.author_info(
+ 'A.M. Kuchling', 'amk@amk.ca'), }
+
+copyright = """\
+Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+ © 2013 Andrej B.
+ © 2013 LEAP Encryption Access Project
+ © 2008-2012 Vinay Sajip
+ © 2005 Steve Traugott
+ © 2004 A.M. Kuchling
+All rights reserved.
+See included LICENSE or ``print(gnupg.__license__)`` for full license."""
+
+disclaimer = """\
+This file is part of python-gnupg, a Python wrapper around GnuPG.
+%s
+
+This program is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option)
+any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+""" % (copyright,)
+
+txcopyright = """\
+Where stated, parts of this program were taken from Twisted, which is
+licensed as follows:
+
+Twisted, the Framework of Your Internet
+Copyright © 2001-2013 Twisted Matrix Laboratories.
+See LICENSE for details.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""
+
+
+GPLv3_text = """\
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+
+ BEGIN ORIGINAL LICENSE TEXT
+
+Copyright (c) 2008-2012 by Vinay Sajip.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * The name(s) of the copyright holder(s) may not be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ END ORIGINAL LICENSE TEXT
+"""
+
+full_text = "%s\n\n%s\n\n%s" % (disclaimer, txcopyright, GPLv3_text)
diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py
new file mode 100644
index 0000000..9aa8232
--- /dev/null
+++ b/gnupg/gnupg.py
@@ -0,0 +1,1067 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of python-gnupg, a Python interface to GnuPG.
+# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
+# © 2013 Andrej B.
+# © 2013 LEAP Encryption Access Project
+# © 2008-2012 Vinay Sajip
+# © 2005 Steve Traugott
+# © 2004 A.M. Kuchling
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
+
+"""gnupg.py
+===========
+A Python interface to GnuPG.
+
+.. moduleauthor:: Isis Lovecruft <isis@patternsinthevoid.net>
+ see also :attr:`gnupg.__authors__`
+.. license:: see :attr:`gnupg.__license__`
+.. info:: https://github.com/isislovecruft/python-gnupg
+"""
+
+from __future__ import absolute_import
+from codecs import open as open
+
+import encodings
+import functools
+import os
+import re
+import textwrap
+
+try:
+ from io import StringIO
+except ImportError:
+ from cStringIO import StringIO
+
+#: see :pep:`328` http://docs.python.org/2.5/whatsnew/pep-328.html
+from . import _parsers
+from . import _util
+from . import _trust
+from ._meta import GPGBase
+from ._parsers import _fix_unsafe
+from ._util import _is_list_or_tuple
+from ._util import _is_stream
+from ._util import _make_binary_stream
+from ._util import log
+
+
+class GPG(GPGBase):
+ """Python interface for handling interactions with GnuPG, including keyfile
+ generation, keyring maintainance, import and export, encryption and
+ decryption, sending to and recieving from keyservers, and signing and
+ verification.
+ """
+
+ #: The number of simultaneous keyids we should list operations like
+ #: '--list-sigs' to:
+ _batch_limit = 25
+
+ def __init__(self, binary=None, homedir=None, verbose=False,
+ use_agent=False, keyring=None, secring=None,
+ options=None):
+ """Initialize a GnuPG process wrapper.
+
+ :param str binary: Name for GnuPG binary executable. If the absolute
+ path is not given, the environment variable
+ ``$PATH`` is searched for the executable and
+ checked that the real uid/gid of the user has
+ sufficient permissions.
+
+ :param str homedir: Full pathname to directory containing the public
+ and private keyrings. Default is whatever GnuPG
+ defaults to.
+
+ :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
+ the list of valid options. If False, debug output is
+ not generated by the GnuPG binary. If True, defaults
+ to ``--debug-level basic.``
+
+ :param str keyring: Name of keyring file containing public key data.
+ If unspecified, defaults to :file:`pubring.gpg` in
+ the **homedir** directory.
+
+ :param str secring: Name of alternative secret keyring file to use. If
+ left unspecified, this will default to using
+ :file:`secring.gpg` in the **homedir** directory,
+ and create that file if it does not exist.
+
+ :param list options: A list of additional options to pass to the GnuPG
+ binary.
+
+ :raises: A :exc:`~exceptions.RuntimeError` with explanation message
+ if there is a problem invoking GnuPG.
+
+ Example:
+
+ >>> import gnupg
+ GnuPG logging disabled...
+ >>> gpg = gnupg.GPG(homedir='doctests')
+ >>> gpg.keyring
+ './doctests/pubring.gpg'
+ >>> gpg.secring
+ './doctests/secring.gpg'
+ >>> gpg.use_agent
+ False
+ >>> gpg.binary
+ '/usr/bin/gpg'
+ """
+
+ super(GPG, self).__init__(
+ binary=binary,
+ home=homedir,
+ keyring=keyring,
+ secring=secring,
+ options=options,
+ verbose=verbose,
+ use_agent=use_agent,)
+
+ log.info(textwrap.dedent("""
+ Initialised settings:
+ binary: %s
+ homedir: %s
+ keyring: %s
+ secring: %s
+ default_preference_list: %s
+ keyserver: %s
+ options: %s
+ verbose: %s
+ use_agent: %s
+ """ % (self.binary, self.homedir, self.keyring, self.secring,
+ self.default_preference_list, self.keyserver, self.options,
+ str(self.verbose), str(self.use_agent))))
+
+ self._batch_dir = os.path.join(self.homedir, 'batch-files')
+ self._key_dir = os.path.join(self.homedir, 'generated-keys')
+
+ #: The keyring used in the most recently created batch file
+ self.temp_keyring = None
+ #: The secring used in the most recently created batch file
+ self.temp_secring = None
+ #: The version string of our GnuPG binary
+ self.binary_version = str()
+
+ ## check that everything runs alright, and grab the gpg binary's
+ ## version number while we're at it:
+ proc = self._open_subprocess(["--list-config", "--with-colons"])
+ result = self._result_map['list'](self)
+ self._read_data(proc.stdout, result)
+ if proc.returncode:
+ raise RuntimeError("Error invoking gpg: %s" % result.data)
+
+ version_line = str(result.data).partition(':version:')[2]
+ self.binary_version = version_line.split('\n')[0]
+ log.debug("Using GnuPG version %s" % self.binary_version)
+
+ if _util._is_gpg2:
+ # Make GnuPG>=2.0.0-only methods public:
+ self.fix_trustdb = self._fix_trustdb
+ self.import_ownertrust = self._import_ownertrust
+ self.export_ownertrust = self._export_ownertrust
+
+ # Make sure that the trustdb exists, or else GnuPG will exit with
+ # a fatal error (at least it does with GnuPG>=2.0.0):
+ self._create_trustdb()
+
+ @functools.wraps(_trust._create_trustdb)
+ def _create_trustdb(self):
+ if self.is_gpg2():
+ _trust._create_trustdb(self)
+ else:
+ log.info("Creating the trustdb is only available with GnuPG>=2.x")
+
+ @functools.wraps(_trust.fix_trustdb)
+ def _fix_trustdb(self, trustdb=None):
+ if self.is_gpg2():
+ _trust.fix_trustdb(self)
+ else:
+ log.info("Fixing the trustdb is only available with GnuPG>=2.x")
+
+ @functools.wraps(_trust.import_ownertrust)
+ def _import_ownertrust(self, trustdb=None):
+ if self.is_gpg2():
+ _trust.import_ownertrust(self)
+ else:
+ log.info("Importing ownertrust is only available with GnuPG>=2.x")
+
+ @functools.wraps(_trust.export_ownertrust)
+ def _export_ownertrust(self, trustdb=None):
+ if self.is_gpg2():
+ _trust.export_ownertrust(self)
+ else:
+ log.info("Exporting ownertrust is only available with GnuPG>=2.x")
+
+ def is_gpg1(self):
+ """Returns true if using GnuPG <= 1.x."""
+ return _util._is_gpg1(self.binary_version)
+
+ def is_gpg2(self):
+ """Returns true if using GnuPG >= 2.x."""
+ return _util._is_gpg2(self.binary_version)
+
+ def sign(self, data, **kwargs):
+ """Create a signature for a message string or file.
+
+ Note that this method is not for signing other keys. (In GnuPG's
+ terms, what we all usually call 'keysigning' is actually termed
+ 'certification'...) Even though they are cryptographically the same
+ operation, GnuPG differentiates between them, presumedly because these
+ operations are also the same as the decryption operation. If the
+ ``key_usage``s ``C (certification)``, ``S (sign)``, and ``E
+ (encrypt)``, were all the same key, the key would "wear down" through
+ frequent signing usage -- since signing data is usually done often --
+ meaning that the secret portion of the keypair, also used for
+ decryption in this scenario, would have a statistically higher
+ probability of an adversary obtaining an oracle for it (or for a
+ portion of the rounds in the cipher algorithm, depending on the family
+ of cryptanalytic attack used).
+
+ In simpler terms: this function isn't for signing your friends' keys,
+ it's for something like signing an email.
+
+ :type data: :obj:`str` or :obj:`file`
+ :param data: A string or file stream to sign.
+ :param str default_key: The key to sign with.
+ :param str passphrase: The passphrase to pipe to stdin.
+ :param bool clearsign: If True, create a cleartext signature.
+ :param bool detach: If True, create a detached signature.
+ :param bool binary: If True, do not ascii armour the output.
+ :param str digest_algo: The hash digest to use. Again, to see which
+ hashes your GnuPG is capable of using, do:
+ :command:`$ gpg --with-colons --list-config digestname`.
+ The default, if unspecified, is ``'SHA512'``.
+ """
+ if 'default_key' in kwargs:
+ log.info("Signing message '%r' with keyid: %s"
+ % (data, kwargs['default_key']))
+ else:
+ log.warn("No 'default_key' given! Using first key on secring.")
+
+ if hasattr(data, 'read'):
+ result = self._sign_file(data, **kwargs)
+ elif not _is_stream(data):
+ stream = _make_binary_stream(data, self._encoding)
+ result = self._sign_file(stream, **kwargs)
+ stream.close()
+ else:
+ log.warn("Unable to sign message '%s' with type %s"
+ % (data, type(data)))
+ result = None
+ return result
+
+ def verify(self, data):
+ """Verify the signature on the contents of the string ``data``.
+
+ >>> gpg = GPG(homedir="doctests")
+ >>> input = gpg.gen_key_input(Passphrase='foo')
+ >>> key = gpg.gen_key(input)
+ >>> assert key
+ >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='bar')
+ >>> assert not sig
+ >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='foo')
+ >>> assert sig
+ >>> verify = gpg.verify(sig.data)
+ >>> assert verify
+
+ """
+ f = _make_binary_stream(data, self._encoding)
+ result = self.verify_file(f)
+ f.close()
+ return result
+
+ def verify_file(self, file, sig_file=None):
+ """Verify the signature on the contents of a file or file-like
+ object. Can handle embedded signatures as well as detached
+ signatures. If using detached signatures, the file containing the
+ detached signature should be specified as the ``sig_file``.
+
+ :param file file: A file descriptor object. Its type will be checked
+ with :func:`_util._is_file`.
+
+ :param str sig_file: A file containing the GPG signature data for
+ ``file``. If given, ``file`` is verified via this detached
+ signature.
+ """
+
+ fn = None
+ result = self._result_map['verify'](self)
+
+ if sig_file is None:
+ log.debug("verify_file(): Handling embedded signature")
+ args = ["--verify"]
+ proc = self._open_subprocess(args)
+ writer = _util._threaded_copy_data(file, proc.stdin)
+ self._collect_output(proc, result, writer, stdin=proc.stdin)
+ else:
+ if not _util._is_file(sig_file):
+ log.debug("verify_file(): '%r' is not a file" % sig_file)
+ return result
+ log.debug('verify_file(): Handling detached verification')
+ sig_fh = None
+ data_fh = None
+ try:
+ sig_fh = open(sig_file, 'rb')
+ data_fh = open(file, 'rb')
+ args = ["--verify %s -" % sig_fh.name]
+ proc = self._open_subprocess(args)
+ writer = _util._threaded_copy_data(data_fh, proc.stdin)
+ self._collect_output(proc, result, writer, stdin=proc.stdin)
+ finally:
+ if sig_fh and not sig_fh.closed:
+ sig_fh.close()
+ if data_fh and not data_fh.closed:
+ data_fh.close()
+ return result
+
+ def import_keys(self, key_data):
+ """
+ Import the key_data into our keyring.
+
+ >>> import shutil
+ >>> shutil.rmtree("doctests")
+ >>> gpg = gnupg.GPG(homedir="doctests")
+ >>> inpt = gpg.gen_key_input()
+ >>> key1 = gpg.gen_key(inpt)
+ >>> print1 = str(key1.fingerprint)
+ >>> pubkey1 = gpg.export_keys(print1)
+ >>> seckey1 = gpg.export_keys(print1,secret=True)
+ >>> key2 = gpg.gen_key(inpt)
+ >>> print2 = key2.fingerprint
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> pubkeys = gpg.list_keys()
+ >>> assert print1 in seckeys.fingerprints
+ >>> assert print1 in pubkeys.fingerprints
+ >>> str(gpg.delete_keys(print1))
+ 'Must delete secret key first'
+ >>> str(gpg.delete_keys(print1,secret=True))
+ 'ok'
+ >>> str(gpg.delete_keys(print1))
+ 'ok'
+ >>> pubkeys = gpg.list_keys()
+ >>> assert not print1 in pubkeys.fingerprints
+ >>> result = gpg.import_keys(pubkey1)
+ >>> pubkeys = gpg.list_keys()
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> assert not print1 in seckeys.fingerprints
+ >>> assert print1 in pubkeys.fingerprints
+ >>> result = gpg.import_keys(seckey1)
+ >>> assert result
+ >>> seckeys = gpg.list_keys(secret=True)
+ >>> assert print1 in seckeys.fingerprints
+ """
+ ## xxx need way to validate that key_data is actually a valid GPG key
+ ## it might be possible to use --list-packets and parse the output
+
+ result = self._result_map['import'](self)
+ log.info('Importing: %r', key_data[:256])
+ data = _make_binary_stream(key_data, self._encoding)
+ self._handle_io(['--import'], data, result, binary=True)
+ data.close()
+ return result
+
+ def recv_keys(self, *keyids, **kwargs):
+ """Import keys from a keyserver.
+
+ >>> gpg = gnupg.GPG(homedir="doctests")
+ >>> key = gpg.recv_keys('hkp://pgp.mit.edu', '3FF0DB166A7476EA')
+ >>> assert key
+
+ :param str keyids: Each ``keyids`` argument should be a string
+ containing a keyid to request.
+ :param str keyserver: The keyserver to request the ``keyids`` from;
+ defaults to `gnupg.GPG.keyserver`.
+ """
+ if keyids:
+ keys = ' '.join([key for key in keyids])
+ return self._recv_keys(keys, **kwargs)
+ else:
+ log.error("No keyids requested for --recv-keys!")
+
+ def delete_keys(self, fingerprints, secret=False, subkeys=False):
+ """Delete a key, or list of keys, from the current keyring.
+
+ The keys must be referred to by their full fingerprints for GnuPG to
+ delete them. If ``secret=True``, the corresponding secret keyring will
+ be deleted from :obj:`.secring`.
+
+ :type fingerprints: :obj:`str` or :obj:`list` or :obj:`tuple`
+ :param fingerprints: A string, or a list/tuple of strings,
+ representing the fingerprint(s) for the key(s)
+ to delete.
+
+ :param bool secret: If True, delete the corresponding secret key(s)
+ also. (default: False)
+
+ :param bool subkeys: If True, delete the secret subkey first, then the
+ public key. (default: False) Same as:
+ :command:`$gpg --delete-secret-and-public-key 0x12345678`.
+ """
+ which = 'keys'
+ if secret:
+ which = 'secret-keys'
+ if subkeys:
+ which = 'secret-and-public-keys'
+
+ if _is_list_or_tuple(fingerprints):
+ fingerprints = ' '.join(fingerprints)
+
+ args = ['--batch']
+ args.append("--delete-{0} {1}".format(which, fingerprints))
+
+ result = self._result_map['delete'](self)
+ p = self._open_subprocess(args)
+ self._collect_output(p, result, stdin=p.stdin)
+ return result
+
+ def export_keys(self, keyids, secret=False, subkeys=False):
+ """Export the indicated ``keyids``.
+
+ :param str keyids: A keyid or fingerprint in any format that GnuPG will
+ accept.
+ :param bool secret: If True, export only the secret key.
+ :param bool subkeys: If True, export the secret subkeys.
+ """
+ which = ''
+ if subkeys:
+ which = '-secret-subkeys'
+ elif secret:
+ which = '-secret-keys'
+
+ if _is_list_or_tuple(keyids):
+ keyids = ' '.join(['%s' % k for k in keyids])
+
+ args = ["--armor"]
+ args.append("--export{0} {1}".format(which, keyids))
+
+ p = self._open_subprocess(args)
+ ## gpg --export produces no status-fd output; stdout will be empty in
+ ## case of failure
+ #stdout, stderr = p.communicate()
+ result = self._result_map['delete'](self) # any result will do
+ self._collect_output(p, result, stdin=p.stdin)
+ log.debug('Exported:%s%r' % (os.linesep, result.data))
+ return result.data.decode(self._encoding, self._decode_errors)
+
+ def list_keys(self, secret=False):
+ """List the keys currently in the keyring.
+
+ The GnuPG option '--show-photos', according to the GnuPG manual, "does
+ not work with --with-colons", but since we can't rely on all versions
+ of GnuPG to explicitly handle this correctly, we should probably
+ include it in the args.
+
+ >>> import shutil
+ >>> shutil.rmtree("doctests")
+ >>> gpg = GPG(homedir="doctests")
+ >>> input = gpg.gen_key_input()
+ >>> result = gpg.gen_key(input)
+ >>> print1 = result.fingerprint
+ >>> result = gpg.gen_key(input)
+ >>> print2 = result.fingerprint
+ >>> pubkeys = gpg.list_keys()
+ >>> assert print1 in pubkeys.fingerprints
+ >>> assert print2 in pubkeys.fingerprints
+ """
+
+ which = 'public-keys'
+ if secret:
+ which = 'secret-keys'
+ args = "--list-%s --fixed-list-mode --fingerprint " % (which,)
+ args += "--with-colons --list-options no-show-photos"
+ args = [args]
+ p = self._open_subprocess(args)
+
+ # there might be some status thingumy here I should handle... (amk)
+ # ...nope, unless you care about expired sigs or keys (stevegt)
+
+ # Get the response information
+ result = self._result_map['list'](self)
+ self._collect_output(p, result, stdin=p.stdin)
+ lines = result.data.decode(self._encoding,
+ self._decode_errors).splitlines()
+ valid_keywords = 'pub uid sec fpr sub'.split()
+ for line in lines:
+ if self.verbose:
+ print(line)
+ log.debug("%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_packets(self, raw_data):
+ """List the packet contents of a file."""
+ args = ["--list-packets"]
+ result = self._result_map['packets'](self)
+ self._handle_io(args, _make_binary_stream(raw_data, self._encoding),
+ result)
+ return result
+
+ def list_sigs(self, *keyids):
+ """Get the signatures for each of the ``keyids``.
+
+ >>> import gnupg
+ >>> gpg = gnupg.GPG(homedir="doctests")
+ >>> key_input = gpg.gen_key_input()
+ >>> key = gpg.gen_key(key_input)
+ >>> assert key.fingerprint
+
+ :rtype: dict
+ :returns: A dictionary whose keys are the original keyid parameters,
+ and whose values are lists of signatures.
+ """
+ if len(keyids) > self._batch_limit:
+ raise ValueError(
+ "List signatures is limited to %d keyids simultaneously"
+ % self._batch_limit)
+
+ args = ["--with-colons", "--fixed-list-mode", "--list-sigs"]
+
+ for key in keyids:
+ args.append(key)
+
+ proc = self._open_subprocess(args)
+ result = self._result_map['list'](self)
+ self._collect_output(proc, result, stdin=proc.stdin)
+ return result
+
+ def gen_key(self, input):
+ """Generate a GnuPG key through batch file key generation. See
+ :meth:`GPG.gen_key_input()` for creating the control input.
+
+ >>> import gnupg
+ >>> gpg = gnupg.GPG(homedir="doctests")
+ >>> key_input = gpg.gen_key_input()
+ >>> key = gpg.gen_key(key_input)
+ >>> assert key.fingerprint
+
+ :param dict input: A dictionary of parameters and values for the new
+ key.
+ :returns: The result mapping with details of the new key, which is a
+ :class:`GenKey <gnupg._parsers.GenKey>` object.
+ """
+ args = ["--gen-key --batch"]
+ key = self._result_map['generate'](self)
+ f = _make_binary_stream(input, self._encoding)
+ self._handle_io(args, f, key, binary=True)
+ f.close()
+
+ fpr = str(key.fingerprint)
+ if len(fpr) == 20:
+ for d in map(lambda x: os.path.dirname(x),
+ [self.temp_keyring, self.temp_secring]):
+ if not os.path.exists(d):
+ os.makedirs(d)
+
+ if self.temp_keyring:
+ if os.path.isfile(self.temp_keyring):
+ prefix = os.path.join(self.temp_keyring, fpr)
+ try: os.rename(self.temp_keyring, prefix+".pubring")
+ except OSError as ose: log.error(str(ose))
+
+ if self.temp_secring:
+ if os.path.isfile(self.temp_secring):
+ prefix = os.path.join(self.temp_secring, fpr)
+ try: os.rename(self.temp_secring, prefix+".secring")
+ except OSError as ose: log.error(str(ose))
+
+ log.info("Key created. Fingerprint: %s" % fpr)
+ key.keyring = self.temp_keyring
+ key.secring = self.temp_secring
+ self.temp_keyring = None
+ self.temp_secring = None
+
+ return key
+
+ def gen_key_input(self, separate_keyring=False, save_batchfile=False,
+ testing=False, **kwargs):
+ """Generate a batch file for input to :meth:`~gnupg.GPG.gen_key`.
+
+ The GnuPG batch file key generation feature allows unattended key
+ generation by creating a file with special syntax and then providing it
+ to: :command:`gpg --gen-key --batch`. Batch files look like this:
+
+ | Name-Real: Alice
+ | Name-Email: alice@inter.net
+ | Expire-Date: 2014-04-01
+ | Key-Type: RSA
+ | Key-Length: 4096
+ | Key-Usage: cert
+ | Subkey-Type: RSA
+ | Subkey-Length: 4096
+ | Subkey-Usage: encrypt,sign,auth
+ | Passphrase: sekrit
+ | %pubring foo.gpg
+ | %secring sec.gpg
+ | %commit
+
+ which is what this function creates for you. All of the available,
+ non-control parameters are detailed below (control parameters are the
+ ones which begin with a '%'). For example, to generate the batch file
+ example above, use like this:
+
+ >>> import gnupg
+ GnuPG logging disabled...
+ >>> from __future__ import print_function
+ >>> gpg = gnupg.GPG(homedir='doctests')
+ >>> alice = { 'name_real': 'Alice',
+ ... 'name_email': 'alice@inter.net',
+ ... 'expire_date': '2014-04-01',
+ ... 'key_type': 'RSA',
+ ... 'key_length': 4096,
+ ... 'key_usage': '',
+ ... 'subkey_type': 'RSA',
+ ... 'subkey_length': 4096,
+ ... 'subkey_usage': 'encrypt,sign,auth',
+ ... 'passphrase': 'sekrit'}
+ >>> alice_input = gpg.gen_key_input(**alice)
+ >>> print(alice_input)
+ Key-Type: RSA
+ Subkey-Type: RSA
+ Subkey-Usage: encrypt,sign,auth
+ Expire-Date: 2014-04-01
+ Passphrase: sekrit
+ Name-Real: Alice
+ Name-Email: alice@inter.net
+ Key-Length: 4096
+ Subkey-Length: 4096
+ %pubring ./doctests/alice.pubring.gpg
+ %secring ./doctests/alice.secring.gpg
+ %commit
+ <BLANKLINE>
+ >>> alice_key = gpg.gen_key(alice_input)
+ >>> assert alice_key is not None
+ >>> assert alice_key.fingerprint is not None
+ >>> message = "no one else can read my sekrit message"
+ >>> encrypted = gpg.encrypt(message, alice_key.fingerprint)
+ >>> assert isinstance(encrypted.data, str)
+
+ :param bool separate_keyring: Specify for the new key to be written to
+ a separate pubring.gpg and secring.gpg. If True,
+ :meth:`~gnupg.GPG.gen_key` will automatically rename the separate
+ keyring and secring to whatever the fingerprint of the generated
+ key ends up being, suffixed with '.pubring' and '.secring'
+ respectively.
+
+ :param bool save_batchfile: Save a copy of the generated batch file to
+ disk in a file named <name_real>.batch, where <name_real> is the
+ ``name_real`` parameter stripped of punctuation, spaces, and
+ non-ascii characters.
+
+ :param bool testing: Uses a faster, albeit insecure random number
+ generator to create keys. This should only be used for testing
+ purposes, for keys which are going to be created and then soon
+ after destroyed, and never for the generation of actual use keys.
+
+ :param str name_real: The name field of the UID in the generated key.
+ :param str name_comment: The comment in the UID of the generated key.
+
+ :param str name_email: The email in the UID of the generated key.
+ (default: ``$USER`` @ :command:`hostname` ) Remember to use UTF-8
+ encoding for the entirety of the UID. At least one of
+ ``name_real``, ``name_comment``, or ``name_email`` must be
+ provided, or else no user ID is created.
+
+ :param str key_type: One of 'RSA', 'DSA', 'ELG-E', or 'default'.
+ (default: 'RSA', if using GnuPG v1.x, otherwise 'default') Starts
+ a new parameter block by giving the type of the primary key. The
+ algorithm must be capable of signing. This is a required
+ parameter. The algorithm may either be an OpenPGP algorithm number
+ or a string with the algorithm name. The special value ‘default’
+ may be used for algo to create the default key type; in this case
+ a ``key_usage`` should not be given and 'default' must also be
+ used for ``subkey_type``.
+
+ :param int key_length: The requested length of the generated key in
+ bits. (Default: 4096)
+
+ :param str key_grip: hexstring This is an optional hexidecimal string
+ which is used to generate a CSR or certificate for an already
+ existing key. ``key_length`` will be ignored if this parameter
+ is given.
+
+ :param str key_usage: Space or comma delimited string of key
+ usages. Allowed values are ‘encrypt’, ‘sign’, and ‘auth’. This is
+ used to generate the key flags. Please make sure that the
+ algorithm is capable of this usage. Note that OpenPGP requires
+ that all primary keys are capable of certification, so no matter
+ what usage is given here, the ‘cert’ flag will be on. If no
+ ‘Key-Usage’ is specified and the ‘Key-Type’ is not ‘default’, all
+ allowed usages for that particular algorithm are used; if it is
+ not given but ‘default’ is used the usage will be ‘sign’.
+
+ :param str subkey_type: This generates a secondary key
+ (subkey). Currently only one subkey can be handled. See also
+ ``key_type`` above.
+
+ :param int subkey_length: The length of the secondary subkey in bits.
+
+ :param str subkey_usage: Key usage for a subkey; similar to
+ ``key_usage``.
+
+ :type expire_date: :obj:`int` or :obj:`str`
+ :param expire_date: Can be specified as an iso-date or as
+ <int>[d|w|m|y] Set the expiration date for the key (and the
+ subkey). It may either be entered in ISO date format (2000-08-15)
+ or as number of days, weeks, month or years. The special notation
+ "seconds=N" is also allowed to directly give an Epoch
+ value. Without a letter days are assumed. Note that there is no
+ check done on the overflow of the type used by OpenPGP for
+ timestamps. Thus you better make sure that the given value make
+ sense. Although OpenPGP works with time intervals, GnuPG uses an
+ absolute value internally and thus the last year we can represent
+ is 2105.
+
+ :param str creation_date: Set the creation date of the key as stored
+ in the key information and which is also part of the fingerprint
+ calculation. Either a date like "1986-04-26" or a full timestamp
+ like "19860426T042640" may be used. The time is considered to be
+ UTC. If it is not given the current time is used.
+
+ :param str passphrase: The passphrase for the new key. The default is
+ to not use any passphrase. Note that GnuPG>=2.1.x will not allow
+ you to specify a passphrase for batch key generation -- GnuPG will
+ ignore the **passphrase** parameter, stop, and ask the user for
+ the new passphrase. However, we can put the command
+ ``%no-protection`` into the batch key generation file to allow a
+ passwordless key to be created, which can then have its passphrase
+ set later with ``--edit-key``.
+
+ :param str preferences: Set the cipher, hash, and compression
+ preference values for this key. This expects the same type of
+ string as the sub-command ‘setpref’ in the --edit-key menu.
+
+ :param str revoker: Should be given as 'algo:fpr' (case sensitive).
+ Add a designated revoker to the generated key. Algo is the public
+ key algorithm of the designated revoker (i.e. RSA=1, DSA=17, etc.)
+ fpr is the fingerprint of the designated revoker. The optional
+ ‘sensitive’ flag marks the designated revoker as sensitive
+ information. Only v4 keys may be designated revokers.
+
+ :param str keyserver: This is an optional parameter that specifies the
+ preferred keyserver URL for the key.
+
+ :param str handle: This is an optional parameter only used with the
+ status lines ``KEY_CREATED`` and ``KEY_NOT_CREATED``. string may
+ be up to 100 characters and should not contain spaces. It is
+ useful for batch key generation to associate a key parameter block
+ with a status line.
+
+ :rtype: str
+ :returns: A suitable input string for the :meth:`GPG.gen_key` method,
+ the latter of which will create the new keypair.
+
+ See `this GnuPG Manual section`__ for more details.
+
+ __ http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html
+ """
+ #: A boolean for determining whether to set subkey_type to 'default'
+ default_type = False
+
+ parms = {}
+
+ ## if using GnuPG version 1.x, then set the default 'Key-Type' to
+ ## 'RSA' because it doesn't understand 'default'
+ parms.setdefault('Key-Type', 'default')
+ if _util._is_gpg1(self.binary_version):
+ parms.setdefault('Key-Type', 'RSA')
+ log.debug("GnuPG v%s detected: setting default key type to %s."
+ % (self.binary_version, parms['Key-Type']))
+ parms.setdefault('Key-Length', 4096)
+ parms.setdefault('Name-Real', "Autogenerated Key")
+ parms.setdefault('Expire-Date', _util._next_year())
+
+ name_email = kwargs.get('name_email')
+ uidemail = _util.create_uid_email(name_email)
+ parms.setdefault('Name-Email', uidemail)
+
+ if testing:
+ ## This specific comment string is required by (some? all?)
+ ## versions of GnuPG to use the insecure PRNG:
+ parms.setdefault('Name-Comment', 'insecure!')
+
+ for key, val in list(kwargs.items()):
+ 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():
+ parms[key] = val
+
+ ## if Key-Type is 'default', make Subkey-Type also be 'default'
+ if parms['Key-Type'] == 'default':
+ default_type = True
+ for field in ('Key-Usage', 'Subkey-Usage',):
+ try: parms.pop(field) ## toss these out, handle manually
+ except KeyError: pass
+
+ ## Key-Type must come first, followed by length
+ out = "Key-Type: %s\n" % parms.pop('Key-Type')
+ out += "Key-Length: %d\n" % parms.pop('Key-Length')
+ if 'Subkey-Type' in parms.keys():
+ out += "Subkey-Type: %s\n" % parms.pop('Subkey-Type')
+ else:
+ if default_type:
+ out += "Subkey-Type: default\n"
+ if 'Subkey-Length' in parms.keys():
+ out += "Subkey-Length: %s\n" % parms.pop('Subkey-Length')
+
+ for key, val in list(parms.items()):
+ out += "%s: %s\n" % (key, val)
+
+ ## There is a problem where, in the batch files, if the '%%pubring'
+ ## and '%%secring' are given as any static string, i.e. 'pubring.gpg',
+ ## that file will always get rewritten without confirmation, killing
+ ## off any keys we had before. So in the case where we wish to
+ ## generate a bunch of keys and then do stuff with them, we should not
+ ## give 'pubring.gpg' as our keyring file, otherwise we will lose any
+ ## keys we had previously.
+
+ if separate_keyring:
+ ring = str(uidemail + '_' + str(_util._utc_epoch()))
+ self.temp_keyring = os.path.join(self.homedir, ring+'.pubring')
+ self.temp_secring = os.path.join(self.homedir, ring+'.secring')
+ out += "%%pubring %s\n" % self.temp_keyring
+ out += "%%secring %s\n" % self.temp_secring
+
+ if testing:
+ ## see TODO file, tag :compatibility:gen_key_input:
+ ##
+ ## Add version detection before the '%no-protection' flag.
+ out += "%no-protection\n"
+ out += "%transient-key\n"
+
+ out += "%commit\n"
+
+ ## if we've been asked to save a copy of the batch file:
+ if save_batchfile and parms['Name-Email'] != uidemail:
+ asc_uid = encodings.normalize_encoding(parms['Name-Email'])
+ filename = _fix_unsafe(asc_uid) + _util._now() + '.batch'
+ save_as = os.path.join(self._batch_dir, filename)
+ readme = os.path.join(self._batch_dir, 'README')
+
+ if not os.path.exists(self._batch_dir):
+ os.makedirs(self._batch_dir)
+
+ ## the following pulls the link to GnuPG's online batchfile
+ ## documentation from this function's docstring and sticks it
+ ## in a README file in the batch directory:
+
+ if getattr(self.gen_key_input, '__doc__', None) is not None:
+ docs = self.gen_key_input.__doc__
+ else:
+ docs = str() ## docstring=None if run with "python -OO"
+ links = '\n'.join(x.strip() for x in docs.splitlines()[-2:])
+ explain = """
+This directory was created by python-gnupg, on {}, and
+it contains saved batch files, which can be given to GnuPG to automatically
+generate keys. Please see
+{}""".format(_util.now(), links) ## sometimes python is awesome.
+
+ with open(readme, 'a+') as fh:
+ [fh.write(line) for line in explain]
+
+ with open(save_as, 'a+') as batch_file:
+ [batch_file.write(line) for line in out]
+
+ return out
+
+ def encrypt(self, data, *recipients, **kwargs):
+ """Encrypt the message contained in ``data`` to ``recipients``.
+
+ :param str data: The file or bytestream to encrypt.
+
+ :param str recipients: The recipients to encrypt to. Recipients must
+ be specified keyID/fingerprint. Care should be taken in Python2.x
+ to make sure that the given fingerprint is in fact a string and
+ not a unicode object.
+
+ :param str default_key: The keyID/fingerprint of the key to use for
+ signing. If given, ``data`` will be encrypted and signed.
+
+ :param str passphrase: If given, and ``default_key`` is also given,
+ use this passphrase to unlock the secret portion of the
+ ``default_key`` to sign the encrypted ``data``. Otherwise, if
+ ``default_key`` is not given, but ``symmetric=True``, then use
+ this passphrase as the passphrase for symmetric
+ encryption. Signing and symmetric encryption should *not* be
+ combined when sending the ``data`` to other recipients, else the
+ passphrase to the secret key would be shared with them.
+
+ :param bool armor: If True, ascii armor the output; otherwise, the
+ output will be in binary format. (Default: True)
+
+ :param bool encrypt: If True, encrypt the ``data`` using the
+ ``recipients`` public keys. (Default: True)
+
+ :param bool symmetric: If True, encrypt the ``data`` to ``recipients``
+ using a symmetric key. See the ``passphrase`` parameter. Symmetric
+ encryption and public key encryption can be used simultaneously,
+ and will result in a ciphertext which is decryptable with either
+ the symmetric ``passphrase`` or one of the corresponding private
+ keys.
+
+ :param bool always_trust: If True, ignore trust warnings on recipient
+ keys. If False, display trust warnings. (default: True)
+
+ :param str output: The output file to write to. If not specified, the
+ encrypted output is returned, and thus should be stored as an
+ object in Python. For example:
+
+ >>> import shutil
+ >>> import gnupg
+ >>> if os.path.exists("doctests"):
+ ... shutil.rmtree("doctests")
+ >>> gpg = gnupg.GPG(homedir="doctests")
+ >>> key_settings = gpg.gen_key_input(key_type='RSA',
+ ... key_length=1024,
+ ... key_usage='ESCA',
+ ... passphrase='foo')
+ >>> key = gpg.gen_key(key_settings)
+ >>> message = "The crow flies at midnight."
+ >>> encrypted = str(gpg.encrypt(message, key.printprint))
+ >>> assert encrypted != message
+ >>> assert not encrypted.isspace()
+ >>> decrypted = str(gpg.decrypt(encrypted))
+ >>> assert not decrypted.isspace()
+ >>> decrypted
+ 'The crow flies at midnight.'
+
+
+ :param str cipher_algo: The cipher algorithm to use. To see available
+ algorithms with your version of GnuPG, do:
+ :command:`$ gpg --with-colons --list-config ciphername`.
+ The default ``cipher_algo``, if unspecified, is ``'AES256'``.
+
+ :param str digest_algo: The hash digest to use. Again, to see which
+ hashes your GnuPG is capable of using, do:
+ :command:`$ gpg --with-colons --list-config digestname`.
+ The default, if unspecified, is ``'SHA512'``.
+
+ :param str compress_algo: The compression algorithm to use. Can be one
+ of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or ``'Uncompressed'``.
+
+ .. seealso:: :meth:`._encrypt`
+ """
+ stream = _make_binary_stream(data, self._encoding)
+ result = self._encrypt(stream, recipients, **kwargs)
+ stream.close()
+ return result
+
+ def decrypt(self, message, **kwargs):
+ """Decrypt the contents of a string or file-like object ``message``.
+
+ :type message: file or str or :class:`io.BytesIO`
+ :param message: A string or file-like object to decrypt.
+ :param bool always_trust: Instruct GnuPG to ignore trust checks.
+ :param str passphrase: The passphrase for the secret key used for decryption.
+ :param str output: A filename to write the decrypted output to.
+ """
+ stream = _make_binary_stream(message, self._encoding)
+ result = self.decrypt_file(stream, **kwargs)
+ stream.close()
+ return result
+
+ def decrypt_file(self, filename, always_trust=False, passphrase=None,
+ output=None):
+ """Decrypt the contents of a file-like object ``filename`` .
+
+ :param str filename: A file-like object to decrypt.
+ :param bool always_trust: Instruct GnuPG to ignore trust checks.
+ :param str passphrase: The passphrase for the secret key used for decryption.
+ :param str output: A filename to write the decrypted output to.
+ """
+ args = ["--decrypt"]
+ if output: # write the output to a file with the specified name
+ if os.path.exists(output):
+ os.remove(output) # to avoid overwrite confirmation message
+ args.append('--output %s' % output)
+ if always_trust:
+ args.append("--always-trust")
+ result = self._result_map['crypt'](self)
+ self._handle_io(args, filename, result, passphrase, binary=True)
+ log.debug('decrypt result: %r', result.data)
+ return result
+
+class GPGUtilities(object):
+ """Extra tools for working with GnuPG."""
+
+ def __init__(self, gpg):
+ """Initialise extra utility functions."""
+ self._gpg = gpg
+
+ def find_key_by_email(self, email, secret=False):
+ """Find user's key based on their email address.
+
+ :param str email: The email address to search for.
+ :param bool secret: If True, search through secret keyring.
+ """
+ for key in self.list_keys(secret=secret):
+ for uid in key['uids']:
+ if re.search(email, uid):
+ return key
+ raise LookupError("GnuPG public key for email %s not found!" % email)
+
+ def find_key_by_subkey(self, subkey):
+ """Find a key by a fingerprint of one of its subkeys.
+
+ :param str subkey: The fingerprint of the subkey to search for.
+ """
+ for key in self.list_keys():
+ for sub in key['subkeys']:
+ if sub[0] == subkey:
+ return key
+ raise LookupError(
+ "GnuPG public key for subkey %s not found!" % subkey)
+
+ def send_keys(self, keyserver, *keyids):
+ """Send keys to a keyserver."""
+ result = self._result_map['list'](self)
+ log.debug('send_keys: %r', keyids)
+ data = _util._make_binary_stream("", self._encoding)
+ args = ['--keyserver', keyserver, '--send-keys']
+ args.extend(keyids)
+ self._handle_io(args, data, result, binary=True)
+ log.debug('send_keys result: %r', result.__dict__)
+ data.close()
+ return result
+
+ def encrypted_to(self, raw_data):
+ """Return the key to which raw_data is encrypted to."""
+ # TODO: make this support multiple keys.
+ result = self._gpg.list_packets(raw_data)
+ if not result.key:
+ raise LookupError(
+ "Content is not encrypted to a GnuPG key!")
+ try:
+ return self.find_key_by_keyid(result.key)
+ except:
+ return self.find_key_by_subkey(result.key)
+
+ def is_encrypted_sym(self, raw_data):
+ result = self._gpg.list_packets(raw_data)
+ return bool(result.need_passphrase_sym)
+
+ def is_encrypted_asym(self, raw_data):
+ result = self._gpg.list_packets(raw_data)
+ return bool(result.key)
+
+ def is_encrypted(self, raw_data):
+ return self.is_encrypted_asym(raw_data) or self.is_encrypted_sym(raw_data)
+
+if __name__ == "__main__":
+ from .test import test_gnupg
+ test_gnupg.main()