diff options
Diffstat (limited to 'gnupg/gnupg.py')
-rw-r--r-- | gnupg/gnupg.py | 1067 |
1 files changed, 1067 insertions, 0 deletions
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() |