Ejemplo n.º 1
0
def execute_gpg_command(home_dir, initial_args, passphrase=None, data=None):
    '''
        Issue a GPG command in its own worker so there are no concurrency challenges.
    '''

    log = LogFile(filename='goodcrypto.oce.gpg_exec_queue.log')
    gpg_exec = None
    try:
        if initial_args is not None:
            new_args = []
            for arg in initial_args:
                new_args.append(arg)
            initial_args = new_args
            log.write_and_flush('gpg exec: {}'.format(initial_args))

        auto_check_trustdb = gpg_constants.CHECK_TRUSTDB in initial_args

        # different higher levels may try to generate the same key
        # so only allow one key to be generated
        if gpg_constants.GEN_KEY in initial_args:
            command_ready = need_private_key(home_dir, data)
            if not command_ready:
                result_code = gpg_constants.GOOD_RESULT
                gpg_output = gpg_constants.KEY_EXISTS
                gpg_error = None
                log.write_and_flush('{}'.format(gpg_output))

        # if deleting a key, get the fingerprint because gpg
        # only allows deletion in batch mode with the fingerprint
        elif gpg_constants.DELETE_KEYS in initial_args:
            fingerprint = prep_to_delete_key(home_dir, initial_args)
            if fingerprint is not None:
                initial_args = [gpg_constants.DELETE_KEYS, fingerprint]
                log.write_and_flush('ready to delete key: {}'.format(fingerprint))
            command_ready = True

        else:
            command_ready = True

        if command_ready:
            gpg_exec = GPGExec(home_dir, auto_check_trustdb=auto_check_trustdb)
            result_code, gpg_output, gpg_error = gpg_exec.execute(
                initial_args, passphrase, data)

        log.write_and_flush('result code: {}'.format(result_code))
    except JobTimeoutException as job_exception:
        log.write_and_flush('gpg exec {}'.format(str(job_exception)))
        result_code = gpg_constants.TIMED_OUT_RESULT
        gpg_error = str(job_exception)
        gpg_output = None
        log.write_and_flush('EXCEPTION - see syr.exception.log for details')
        record_exception()
    except Exception as exception:
        result_code = gpg_constants.ERROR_RESULT
        gpg_output = None
        gpg_error = str(exception)
        log.write_and_flush('EXCEPTION - see syr.exception.log for details')
        record_exception()

        if gpg_exec is not None and gpg_exec.gpg_home is not None:
            gpg_exec.clear_gpg_lock_files()
            gpg_exec.clear_gpg_tmp_files()

    log.flush()

    return result_code, gpg_output, gpg_error
Ejemplo n.º 2
0
class GPGPlugin(AbstractPlugin):
    '''
        Gnu Privacy Guard crypto plugin.

        Be careful with how you specify a user ID to GPG.
        Case insensitive substring matching is the default.
        For example if you specify the email address "*****@*****.**" as a user ID,
        you will match a user ID such as "*****@*****.**".
        You can specify an exact match on the entire user ID by prefixing
        your user ID spec with "=", e.g. "=John Heinrich <*****@*****.**>".
        Another option is to tell GPG you want an exact match on an email
        address using "<" and ">", e.g. "<*****@*****.**>".

        Debug note: If there seems to be an extra blank line at the top of decrypted text,
        check whether we should be using "passphrase + \r" instead of "passphrase + EOL".
    '''

    DEBUGGING = False

    # use a queue to insure that GPG is only run one instance at a time
    USE_RQ = True

    #  Match email addresses of user IDs. This is the default.
    #  If a user ID does not include "@", acts like CASE_INSENSITVE_MATCH.
    #
    EMAIL_MATCH = 1

    #  Match user IDs exactly.
    EXACT_MATCH = 2

    #
    #  Match case insensitive substrings of user IDs.
    #  This is GPG's default, but not the default for this class.
    #
    CASE_INSENSITVE_MATCH = 3

    GPG_COMMAND_NAME = "gpg"
    GOOD_SIGNATURE_PREFIX = "gpg: Good signature from "
    BAD_SIGNATURE_PREFIX = "gpg: BAD signature from "

    ONE_MINUTE = 60 #  one minute, in seconds
    DEFAULT_TIMEOUT = 10 * ONE_MINUTE

    GPG_HOME_DIR = os.path.join(OCE_DATA_DIR, '.gnupg')


    def __init__(self):
        '''
            Creates a new GPGPlugin object.
        '''

        super(GPGPlugin, self).__init__()

        self.log = LogFile()

        self.name = gpg_constants.ENCRYPTION_NAME
        self._executable_pathname = self.GPG_COMMAND_NAME
        self._user_id_match_method = self.EMAIL_MATCH

        # keep the info so other classes can add dependencies
        self.queue = None
        self.queue_connection = None
        self.job = None

        self.gpg_home = self.GPG_HOME_DIR

    def get_job_count(self):
        '''
            Get the jobs in the queue.
        '''

        return manage_queues.get_job_count(self.get_queue_name(), self.get_queue_port())

    def get_job(self):
        '''
            Get the job from the queue if this plugin uses one.
        '''

        return self.job

    def get_queue_connection(self):
        '''
            Get the connection to the queue if this plugin uses one.
        '''

        if self.queue_connection is None:
            self.queue_connection = Redis(REDIS_HOST, self.get_queue_port())

        return self.queue_connection

    def get_queue(self):
        '''
            Get the queue if this plugin uses one.
        '''

        return self.queue

    def get_queue_name(self):
        '''
            Get the queue's name if this plugin uses one.

            >>> plugin = GPGPlugin()
            >>> plugin.get_queue_name()
            'gpg'
        '''

        return GPG_RQ

    def get_queue_port(self):
        '''
            Get the queue's port if this plugin uses one.

            >>> plugin = GPGPlugin()
            >>> plugin.get_queue_port()
            6381
        '''

        return GPG_REDIS_PORT

    def wait_until_queue_empty(self):
        '''
            Wait until the queue is empty.
        '''

        return manage_queues.wait_until_queue_empty(self.get_queue_name(), self.get_queue_port())

    def clear_failed_queue(self):
        '''
            Clear all the jobs in the failed queue.
        '''

        return manage_queues.clear_failed_queue(self.get_queue_name(), self.get_queue_port())

    def set_user_id_match_method(self, method):
        '''
            Set user ID match method.
        '''

        if method == self.EMAIL_MATCH or method == self.EXACT_MATCH:
            self._user_id_match_method = method


    def get_user_id_match_method(self):
        '''
            Get user ID match method.
            The default is to match email addresses of user IDs.
        '''

        return self._user_id_match_method


    def set_executable(self, pathname):
        '''
            Set executable pathname.

            >>> plugin = GPGPlugin()
            >>> plugin.set_executable('/usr/bin/gpg')
            >>> plugin.get_executable() == '/usr/bin/gpg'
            True
            >>> plugin.set_executable(plugin.GPG_COMMAND_NAME)
        '''

        self._executable_pathname = pathname


    def get_executable(self):
        '''
            Get executable pathname.

            >>> plugin = GPGPlugin()
            >>> plugin.get_executable() != None
            True
        '''

        if self._executable_pathname is None:
            self._executable_pathname = self.GPG_COMMAND_NAME

        return self._executable_pathname


    def get_default_executable(self):
        '''
            Get default executable pathname.

            >>> plugin = GPGPlugin()
            >>> executable = plugin.get_executable()
            >>> executable == 'gpg'
            True
        '''

        return self.GPG_COMMAND_NAME

    def get_crypto_version(self):
        '''
            Get the version of the underlying crypto.

            >>> from shutil import rmtree
            >>> plugin = GPGPlugin()
            >>> original_home_dir = plugin.get_home_dir()
            >>> plugin.set_home_dir('/var/local/projects/goodcrypto/server/data/test_oce/.gnupg')
            True
            >>> plugin.get_crypto_version() is not None
            True
            >>> if os.path.exists('/var/local/projects/goodcrypto/server/data/test_oce/.gnupg'):
            ...     rmtree('/var/local/projects/goodcrypto/server/data/test_oce/.gnupg')
            >>> if os.path.exists('/var/local/projects/goodcrypto/server/data/test_oce'):
            ...     rmtree('/var/local/projects/goodcrypto/server/data/test_oce')
            >>> plugin.set_home_dir(original_home_dir)
            True
        '''

        version_number = None
        try:
            args = [gpg_constants.GET_VERSION]
            result_code, gpg_output, gpg_error= self.gpg_command(args)
            if result_code == gpg_constants.GOOD_RESULT:
                version_number = self._parse_version(gpg_output)
                self.log_message("version number is {}".format(version_number))
        except Exception as exception:
            self.log_message(exception)

        return version_number

    def get_name(self):
        '''
            Get the crypto's short name.

            >>> plugin = GPGPlugin()
            >>> name = plugin.get_name()
            >>> name == gpg_constants.ENCRYPTION_NAME
            True
        '''

        return self.name

    def get_plugin_name(self):
        '''
            Get the plugin's name.

            >>> plugin = GPGPlugin()
            >>> plugin.get_plugin_name().startswith('goodcrypto.oce')
            True
        '''

        return gpg_constants.NAME

    def get_plugin_version(self):
        '''
            Get the version of this plugin's implementation, i.e. the CORBA servant's version.

            >>> plugin = GPGPlugin()
            >>> version = plugin.get_plugin_version()
            >>> version is not None
            True
            >>> version == '0.1'
            True
        '''

        return "0.1"

    def get_user_ids(self):
        '''
            Get list of user IDs with a public key.

            >>> from shutil import rmtree
            >>> plugin = GPGPlugin()
            >>> original_home_dir = plugin.get_home_dir()
            >>> plugin.set_home_dir('/var/local/projects/goodcrypto/server/data/test_oce/.gnupg')
            True
            >>> plugin.get_private_user_ids() is not None
            True
            >>> if os.path.exists('/var/local/projects/goodcrypto/server/data/test_oce/.gnupg'):
            ...     rmtree('/var/local/projects/goodcrypto/server/data/test_oce/.gnupg')
            >>> if os.path.exists('/var/local/projects/goodcrypto/server/data/test_oce'):
            ...     rmtree('/var/local/projects/goodcrypto/server/data/test_oce')
            >>> plugin.set_home_dir(original_home_dir)
            True
        '''

        user_ids = None
        try:
            # we're using --with-colons because we hope that format is less likely to change
            args = [gpg_constants.LIST_PUBLIC_KEYS, gpg_constants.WITH_COLONS]
            result_code, gpg_output, gpg_error= self.gpg_command(args)
            if result_code == gpg_constants.GOOD_RESULT:
                self.log_message('gpg_output: {}'.format(gpg_output))
                self.log_message('gpg_error: {}'.format(gpg_error))
                user_ids = self.parse_user_ids(gpg_constants.PUB_PREFIX, gpg_output)
                self.log_message('{} public user ids'.format(len(user_ids)))

        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return user_ids

    def get_private_user_ids(self):
        '''
            Get list of user IDs with a private key.

            >>> plugin = GPGPlugin()
            >>> plugin.get_private_user_ids() is not None
            True
        '''

        user_ids = None
        try:
            # we're using --with-colons because we hope that format is less likely to change
            args = [gpg_constants.LIST_SECRET_KEYS, gpg_constants.WITH_COLONS]
            result_code, gpg_output, gpg_error= self.gpg_command(args)
            if result_code == gpg_constants.GOOD_RESULT:
                self.log_message('gpg output: {}'.format(gpg_output))
                user_ids = self.parse_user_ids(gpg_constants.SEC_PREFIX, gpg_output)
                self.log_message('{} private user ids'.format(len(user_ids)))
                self.log_message('private user ids: {}'.format(user_ids))

        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return user_ids

    def parse_user_ids(self, header, output_string):
        '''
            Parse the user ids from the output string.
            Intended for internal use only.

            Test the extremes.
            >>> plugin = GPGPlugin()
            >>> user_ids = plugin.parse_user_ids(None, None)
            >>> user_ids is not None
            True
            >>> len(user_ids) <= 0
            True

            >>> # In honor of Lieutenant Assaf, who co-signed letter and refused to serve
            >>> # in operations involving the occupied Palestinian territories because
            >>> # of the widespread surveillance of innocent residents.
            >>> plugin = GPGPlugin()
            >>> user_ids = plugin.parse_user_ids('pub', 'pub:u:4096:1:6BFCCC3E4ED73DC4:2013-09-23:::u:Lieutenant Assaf (gpg key) <*****@*****.**>::scESC:')
            >>> user_ids is not None
            True
            >>> len(user_ids) == 1
            True

            >>> # In honor of First Sergeant Guy, who co-signed letter and refused to serve
            >>> # in operations involving the occupied Palestinian territories because
            >>> # of the widespread surveillance of innocent residents.
            >>> plugin = GPGPlugin()
            >>> user_ids = plugin.parse_user_ids('pub', 'pub:u:4096:1:6BFCCC3E4ED73DC4:2013-09-23:First Sergeant Guy (gpg key) <*****@*****.**>')
            >>> user_ids is not None
            True
            >>> len(user_ids) == 0
            True
        '''

        user_ids = []

        try:
            self.log_message('output string type: {}'.format(type(output_string)))
            reader = StringIO(initial_value=output_string)
            for line in reader:
                raw_line = line.strip()
                if (raw_line is not None and
                    len(raw_line) > 0 and
                    (raw_line.lower().startswith(header) or raw_line.lower().startswith('uid'))):
                    # In honor of Sergeant First Class Galia, who co-signed letter and refused to serve
                    # in operations involving the occupied Palestinian territories because
                    # of the widespread surveillance of innocent residents.
                    # results look like this for public keys:
                    #tru::1:1379973353:0:3:1:5
                    #pub:u:4096:1:6BFCCC3E4ED73DC4:2013-09-23:::u:Sergeant First Class Galia <*****@*****.**>::scESC:
                    #uid:-::::2014-06-14::47764CA1D105D5C9D7F023D021203254D66E1C10::mark burdett <*****@*****.**>:
                    #sub:u:4096:1:5215AA2CAF37F286:2013-09-23::::::e:

                    # In honor of Lieutenant Gilad, who co-signed letter and refused to serve
                    # in operations involving the occupied Palestinian territories because
                    # of the widespread surveillance of innocent residents.
                    # results look like this for secret keys:
                    #sec::1024:17:82569302F49264B9:2013-10-14::::Lieutenant Gilad <*****@*****.**>:::
                    #ssb::1024:16:0E298674A69943BF:2013-10-14:::::::
                    elements = raw_line.split(':')
                    if elements and len(elements) > 9:
                        address = get_email(elements[9])
                        user_ids.append(address)
        except TypeError as type_error:
            self.handle_unexpected_exception(type_error)
        except Exception as exception:
            self.handle_unexpected_exception(exception)

        if self.DEBUGGING:
            self.log_message("{} user ids: {}".format(header, user_ids))

        return user_ids

    def is_available(self):
        '''
            Determine if the crypto app is installed.

            >>> plugin = GPGPlugin()
            >>> plugin.is_available()
            True
        '''

        installed = False
        try:
            #  if we can get the version, then the app's installed
            version = self.get_crypto_version()
            if version != None:
                if len(version.strip()) > 0:
                    installed = True

                    # make sure the home directory is defined and exists
                    self.get_home_dir()
                else:
                    self.log_message('unable to get version while trying to verify gpg installed.')

        except Exception:
            self.log_message("unable to get version so assume not installed")
            self.log_message('EXCEPTION - see syr.exception.log for details')
            record_exception()

        self.log_message("GPG's back end app is installed: {}".format(installed))

        return installed

    def get_signer(self, data):
        '''
            Get signer of data.

            Test a few cases known to fail. See the unittests for more robust examples.
            >>> plugin = GPGPlugin()
            >>> signer = plugin.get_signer('Test unsigned data')
            >>> signer = plugin.get_signer(None)
        '''

        signer = None
        try:
            args = [gpg_constants.VERIFY]
            if data is None or len(data.strip()) <= 0:
                self.log_message('data for signer was not defined')
            else:
                result_code, gpg_output, gpg_error= self.gpg_command(args, data=data)
                if result_code == gpg_constants.GOOD_RESULT:
                    signer = self._parse_signer(gpg_error)
                else:
                    # if this is a detached sig, then we need to pass it once as a file and once as stdin
                    with NamedTemporaryFile(mode='wt', dir='/tmp', delete=False) as f:
                        f.write(data)
                        filename = f.name
                    args = [gpg_constants.VERIFY, filename, '-']
                    result_code, gpg_output, gpg_error= self.gpg_command(args, data=data)
                    if result_code == gpg_constants.GOOD_RESULT or result_code == 1:
                        signer = self._parse_signer(gpg_error)
                    os.remove(filename)
        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return signer

    def decrypt(self, data, passphrase):
        '''
            Decrypt data and return the signer, if there is one.

            Test a few cases known to fail. See the unittests for more robust examples.
            >>> plugin = GPGPlugin()
            >>> decrypted_data, signed_by, result_code = plugin.decrypt(
            ...    'This is a test', 'a secret passphrase')
            >>> decrypted_data is None
            True
        '''

        GOOD_SIG = 'Good signature from'
        UNKNOWN_SIG = "gpg: Can't check signature: public key not found"
        DECRYPT_FAILED = 'decryption failed:'
        DECRYPT_MESSAGE_FAILED = 'decrypt_message failed'
        INVALID_PACKET = 'invalid packet'
        NO_VALID_DATA = 'no valid OpenPGP data found'

        decrypted_data = signed_by = None
        result_code = gpg_constants.ERROR_RESULT
        try:
            if self.DEBUGGING:
                self.log_message("decrypting:\n{}".format(data))

            if passphrase is None or data is None:
                self.log_message('unable to decrypt because key info missing')
            else:
                args = [gpg_constants.DECRYPT_DATA, gpg_constants.OPEN_PGP]
                result_code, gpg_output, gpg_error= self.gpg_command(
                    args, passphrase=passphrase, data=data)

                if result_code == gpg_constants.GOOD_RESULT:
                    decrypted_data = gpg_output
                    if gpg_error is not None and GOOD_SIG in gpg_error:
                        signature = gpg_error[gpg_error.find(GOOD_SIG) + len(GOOD_SIG):]
                        signature = signature.split('\n')[0].strip()
                        signed_by = signature.strip('"')
                        self.log_message("decrypted data, with good signature from {}".format(signed_by))

                elif result_code == gpg_constants.CONDITIONAL_RESULT:

                    # if an error reported, but it was just a unknown sig, accept the decryption
                    if gpg_error is not None:
                        if UNKNOWN_SIG in gpg_error:
                            self.log_message("decrypted data, with unknown signature")
                            decrypted_data = gpg_output
                        else:
                            self.log_message("gpg error: {}".format(gpg_error))
                            result_code = gpg_constants.ERROR_RESULT
                else:
                    self.log_message("gpg_error: {}".format(gpg_error))

            if self.DEBUGGING:
                self.log_message("decrypted data:\n{}".format(decrypted_data))

        except Exception as exception:
            self.handle_unexpected_exception(exception)

        self.log_message("decrypted data: {} / result code: {}".format(decrypted_data is not None, result_code))

        return decrypted_data, signed_by, result_code

    def encrypt_and_armor(self, data, to_user_id, charset=None):
        '''
            Encrypt and armor data with the public key indicated by to_user_id.

            >>> # Test a few cases known to fail. See the unittests for more robust examples.
            >>> # In honor of First Sergeant Amit, who publicly denounced and refused to serve in operations
            >>> # involving the occupied Palestinian territories because of the widespread surveillance of innocent residents.
            >>> plugin = GPGPlugin()
            >>> encrypted_data, error_message = plugin.encrypt_and_armor('This is a test', '*****@*****.**')
            >>> encrypted_data is None
            True
            >>> 'public key not found' in error_message
            True
        '''

        encrypted_data = error_message = None
        try:
            if to_user_id is None or data is None or len(data.strip()) <= 0:
                self.log_message('unable to encrypt and armor because key info missing')
            else:
                self.log_message('encrypting and armoring to "{}"'.format(to_user_id))
                self.log_data(data)

                # we could use MIME, but for now keep it readable
                args = [gpg_constants.ENCRYPT_DATA, gpg_constants.ARMOR_DATA, gpg_constants.OPEN_PGP,
                  gpg_constants.RECIPIENT, self.get_user_id_spec(to_user_id)]
                if charset is not None:
                    args.append(gpg_constants.CHARSET)
                    args.append(charset)
                result_code, gpg_output, gpg_error= self.gpg_command(args, data=data)
                if result_code == gpg_constants.GOOD_RESULT:
                    encrypted_data = gpg_output
                else:
                    if gpg_error:
                        error_message = gpg_error
                    if gpg_output and error_message is None:
                        error_message = gpg_output
        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return encrypted_data, error_message

    def sign(self, data, user_id, passphrase):
        '''
            Sign data with the private key indicated by user id.
            Return the signed data or None if the signing fails.

            >>> from goodcrypto.oce import test_constants
            >>> plugin = GPGPlugin()
            >>> signed_data, error_message = plugin.sign(test_constants.TEST_DATA_STRING,
            ...   test_constants.EDWARD_LOCAL_USER, test_constants.EDWARD_PASSPHRASE)
            >>> signed_data is not None
            True
            >>> len(signed_data) > 0
            True
            >>> error_message is None
            True
        '''

        signed_data = error_message = None
        try:
            if passphrase is None or data is None or len(data.strip()) <= 0:
                self.log_message('could not sign because missing key info')
            else:
                user = self.get_user_id_spec(user_id)
                args = [gpg_constants.CLEAR_SIGN, gpg_constants.LOCAL_USER, user]
                result_code, gpg_output, gpg_error= self.gpg_command(
                   args, passphrase=passphrase, data=data)
                if result_code == gpg_constants.GOOD_RESULT:
                    signed_data = gpg_output
                    self.log_message('signed by "{}"'.format(user))
                    self.log_data(signed_data, "signed data")
                else:
                    if gpg_error:
                        error_message = gpg_error
                    if gpg_output and error_message is None:
                        error_message = gpg_output

                    if error_message is not None and 'using subkey' in error_message:
                        i = error_message.find('\n')
                        if i > 0:
                            error_message = error_message[i:]
        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return signed_data, error_message

    def sign_and_encrypt(self, data, from_user_id, to_user_id, passphrase, clear_sign=False, charset=None):
        '''
            Sign data with the secret key indicated by from_user_id, then encrypt with
            the public key indicated by to_user_id.

            To avoid a security bug in OpenPGP we must sign before encrypting.

            >>> # Test a few cases known to fail. See the unittests for more robust examples.
            >>> # In honor of David Weber, who revealed misconduct in the SEC investigations of
            >>> #   Bernard Madoff and Allen Stanford.
            >>> # In honor of Virgil Grandfield, who uncovered a scandal in which some 50,000 or more
            >>> #  Javanese construction workers were victims of human trafficking on NGO tsunami projects in Aceh.
            >>> plugin = GPGPlugin()
            >>> encrypted_data, error_message = plugin.sign_and_encrypt(
            ...   'This is a test', '*****@*****.**', '*****@*****.**', 'secret')
            >>> encrypted_data is None
            True
            >>> error_message is not None
            True
            >>> encrypted_data, error_message = plugin.sign_and_encrypt(None, '*****@*****.**', '*****@*****.**', 'secret')
            >>> encrypted_data is None
            True
            >>> error_message is not None
            True
        '''

        self.log_message('signing by "{}" and encrypting to "{}'.format(
            self.get_user_id_spec(from_user_id), self.get_user_id_spec(to_user_id)))

        args = [gpg_constants.SIGN, gpg_constants.ENCRYPT_DATA, gpg_constants.OPEN_PGP,
                gpg_constants.LOCAL_USER, self.get_user_id_spec(from_user_id),
                gpg_constants.RECIPIENT, self.get_user_id_spec(to_user_id)]
        if charset is not None:
            args.append(gpg_constants.CHARSET)
            args.append(charset)

        encrypted_data, error_message = self._sign_and_encrypt_now(
            data, from_user_id, to_user_id, passphrase, clear_sign, args)

        return encrypted_data, error_message

    def sign_encrypt_and_armor(self, data, from_user, to_user, passphrase, clear_sign=False, charset=None):
        '''
            Sign data with the secret key indicated by from_user, then encrypt with
            the public key indicated by to_user, then ASCII armor, and finally clear sign it.

            To avoid a security bug in OpenPGP we must sign before encrypting.

            >>> # Test a few cases known to fail. See the unittests for more robust examples.
            >>> # In honor of Ted Siska, who blew the whistle on Ward Diesel Filter Systems for filing false
            >>> #   claims to the US government for work on diesel exhaust filtering systems for fire engines.
            >>> # In honor of Peter Bryce, who revealed Canadian Indian children were being systematically and
            >>> #   deliberately killed in the residential schools in the 1920s.
            >>> plugin = GPGPlugin()
            >>> encrypted_data, error_message = plugin.sign_encrypt_and_armor(
            ...   'This is a test known to fail', '*****@*****.**', '*****@*****.**', 'a secret')
            >>> encrypted_data is None
            True
            >>> error_message is not None
            True
            >>> encrypted_data, error_message = plugin.sign_encrypt_and_armor(None, '*****@*****.**', '*****@*****.**', 'a secret')
            >>> encrypted_data is None
            True
            >>> error_message is not None
            True
        '''

        self.log_message('signing by "{}" and encrypting to "{}" and armoring'.format(
            self.get_user_id_spec(from_user), self.get_user_id_spec(to_user)))
        self.log_data(data)

        args = [gpg_constants.ARMOR_DATA, gpg_constants.SIGN,
                gpg_constants.ENCRYPT_DATA, gpg_constants.OPEN_PGP,
                gpg_constants.LOCAL_USER, self.get_user_id_spec(from_user),
                gpg_constants.RECIPIENT, self.get_user_id_spec(to_user)]
        if charset is not None:
            args.append(gpg_constants.CHARSET)
            args.append(charset)

        encrypted_data, error_message = self._sign_and_encrypt_now(
            data, from_user, to_user, passphrase, clear_sign, args)

        return encrypted_data, error_message

    def verify(self, data, by_user_id):
        '''
            Verify data was signed by the user id.

            >>> from goodcrypto.oce import test_constants
            >>> plugin = GPGPlugin()
            >>> signed_data, __ = plugin.sign(test_constants.TEST_DATA_STRING,
            ...   test_constants.EDWARD_LOCAL_USER, test_constants.EDWARD_PASSPHRASE)
            >>> plugin.verify(signed_data, test_constants.EDWARD_LOCAL_USER)
            True


            >>> # In honor of Karen Silkwood, who was the first nuclear power safety whistleblower.
            >>> from goodcrypto.oce.key.key_factory import KeyFactory
            >>> from goodcrypto.oce import test_constants
            >>> email = '*****@*****.**'
            >>> passcode = 'secret'
            >>> plugin = KeyFactory.get_crypto(gpg_constants.ENCRYPTION_NAME)
            >>> ok, __, __, __ = plugin.create(email, passcode, wait_for_results=True)
            >>> ok
            True
            >>> signed_data, __ = plugin.sign(test_constants.TEST_DATA_STRING, email, passcode)
            >>> plugin.delete(email)
            True
            >>> plugin.verify(signed_data, email)
            False
        '''

        self.log_message('starting to verify "{}" signed data'.format(by_user_id))
        signer = self.get_signer(data)
        if signer is None:
            verified = False
            self.log_message("no signer found")
        else:
            self.log_message('signed by "{}"'.format(signer))
            user_email = get_email(by_user_id)
            signer_email = get_email(signer)
            verified = signer_email == user_email
            if not verified:
                self.log_message('could not verify because signed by "{}" not "{}"'.format(
                    signer_email, user_email))
        self.log_message('verified: {}'.format(verified))

        return verified


    def get_user_id_spec(self, user_id):
        ''' Get user ID spec based on the _user_id_match_method. '''

        if user_id is None:
            user = user_id
        else:
            try:
                user = get_email(user_id)
            except Exception:
                self.log_message('EXCEPTION - see syr.exception.log for details')
                record_exception()
                user = user_id

            # if there's an @ sign and the email address is *not* in angle brackets, then add the brackets
            if (self._user_id_match_method == self.EMAIL_MATCH and
                user.find('@') > 0 and
                user.find('<') < 0 and
                user.find('>') < 0):
                user = "******".format(user)

            # if the match method is exact match and the user doesn't start with an equal sign, prefix =
            elif self._user_id_match_method == self.EXACT_MATCH and not user.startswith("="):
                user = "******" + user

        return user

    def list_packets(self, data, passphrase=None):
        ''' Get a list of packets from the data or None if the data isn't encrypted. '''

        packets = None
        try:
            if data is None or (isinstance(data, str) and len(data.strip()) <= 0):
                self.log_message('no data so no packets')
            else:
                self.log_message('trying to list encrypted packets')
                args = [gpg_constants.LIST_PACKETS]
                result_code, gpg_output, gpg_error= self.gpg_command(
                    args, passphrase=passphrase, data=data)

                if result_code == gpg_constants.GOOD_RESULT:
                    packets = gpg_output
                    self.log_data(packets, "good packets")

                elif result_code == gpg_constants.TIMED_OUT_RESULT:
                    self.log_message('timed out analyzing packets')

                elif gpg_error is not None and gpg_error.find('encrypted with') > 0:
                    packets = gpg_error
                    self.log_data(packets, "bad packets")

        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return packets

    def set_home_dir(self, dirname):
        '''
            Sets the home dir and creates it if it doesn't exist.

            If the dirname doesn't start with /, then prefixes the standard OCE data directory.
            Intended for testing the GPG plugin.
        '''

        command_ok = True

        old_home_dir = self.gpg_home

        if dirname.startswith('/'):
            self.gpg_home = dirname
        else:
            self.gpg_home = os.path.join(OCE_DATA_DIR, dirname)

        if not os.path.exists(self.get_home_dir()):
            self.log_message('unable to change home dir to {}'.format(self.gpg_home))
            self.gpg_home = old_home_dir
            command_ok = False

        return command_ok


    def get_home_dir(self):
        ''' Gets the home dir and create it if it doesn't. '''

        if self.gpg_home == None:
            self.gpg_home = self.GPG_HOME_DIR

        try:
            # create gpg's parent directories, if they don't already exist
            parent_dir = os.path.dirname(self.gpg_home)
            if not os.path.exists(parent_dir):
                statinfo = os.stat(os.path.dirname(parent_dir))
                if statinfo.st_uid == os.geteuid():
                    os.makedirs(parent_dir, 0o770)
                    self.log_message('created parent of home dir: {}'.format(parent_dir))
                else:
                    self.log_message('unable to create parent of home dir as {}: {}'.format(os.geteuid(), parent_dir))

            #  create gpg's home directory, if it doesn't exist already
            if not os.path.exists(self.gpg_home):
                statinfo = os.stat(os.path.dirname(self.gpg_home))
                if statinfo.st_uid == os.geteuid():
                    os.mkdir(self.gpg_home, 0o700)
                    self.log_message('created home dir: {}'.format(self.gpg_home))
        except OSError:
            record_exception()
            self.log_message('EXCEPTION - see syr.exception.log for details')
        except Exception as exception:
            self.log_message(exception)
            self.log_message('EXCEPTION - see syr.exception.log for details')
            record_exception()

        return self.gpg_home


    def gpg_command(self, initial_args, passphrase=None, data=None, wait_for_results=True):
        '''
            Issue a gpg command.

            This should be used internally to the gpg classes instead of directly.
            See the public functions that perform gpg commands for better examples.
        '''

        result_code = gpg_constants.ERROR_RESULT
        gpg_output = None
        gpg_error = None

        if gpg_constants.ENCRYPT_DATA in initial_args:
            command = gpg_constants.ENCRYPT_DATA
        elif gpg_constants.DECRYPT_DATA in initial_args:
            command = gpg_constants.DECRYPT_DATA
        else:
            command = initial_args[0]

        # syr.lock.locked() is only a per-process lock
        # syr.lock has a system wide lock, but it is not well tested
        with locked():
            try:
                self.log_message('--- started gpg command: {} ---'.format(command))
                result_code = gpg_constants.ERROR_RESULT
                gpg_output = None
                gpg_error = None

                result_code, gpg_output, gpg_error = self.activate_queue(
                    command, initial_args, passphrase, data, wait_for_results)
            except JobTimeoutException as job_exception:
                self.log_message('gpg command {}'.format(str(job_exception)))
                result_code = gpg_constants.TIMED_OUT_RESULT
            except Exception:
                self.log_message('EXCEPTION - see syr.exception.log for details')
                record_exception()
                result_code = gpg_constants.ERROR_RESULT
                gpg_output = None
                gpg_error = str(exception)
                self.handle_unexpected_exception(exception)
            finally:
                self.log_message('gpg command result_code: {}'.format(result_code))
                self.log_message('--- finished gpg command: {} ---'.format(command))
                self.log.flush()

        return result_code, gpg_output, gpg_error

    def activate_queue(self, command, initial_args, passphrase, data, wait_for_results):
        ''' Run the command and wait for the results if appropriate. '''

        def wait_until_queued(job_count):
            ''' Wait until the job is queued or timeout. '''
            secs = 0
            while (not self.job.is_queued and
                   not self.job.is_started and
                   not self.job.is_finished ):
                sleep(1)
                secs += 1
            self.log_message('seconds until {} job was queued: {}'.format(self.job.get_id(), secs))

        def wait_for_job(command):
            ''' Wait until the job finishes or fails.

                Gpg (1.x) is not thread safe. We use a queue yo make sure it
                only has one instance at a time. But we sometimes need to wait
                here in the client for an operation to complete. So we
                simulate a remote procedure call.
            '''

            result_code = gpg_constants.ERROR_RESULT
            gpg_output = gpg_error = None

            try:
                # wait for the job
                with elapsed_time() as job_time:
                    while self.job.result is None:
                        sleep(1)
                self.log_message('{} job {} elapsed time: {}'.
                    format(command, self.job.get_id(), job_time))

                if self.job.is_finished:
                    self.log_message('{} job finished'.format(command))
                elif self.job.is_failed:
                    self.log_message('{} job failed'.format(command))

                result_code, gpg_output, gpg_error = self.job.result
                self.log_message('{} job {} result code: {}'.format(command, self.job.get_id(), result_code))
                if self.DEBUGGING:
                    if gpg_output: self.log_message(gpg_output)
                    if gpg_error: self.log_message(gpg_error)

                if result_code == gpg_constants.ERROR_RESULT:
                    self.log_message('job.status: {}'.format(self.job.get_status()))

            except JobTimeoutException as job_exception:
                self.log_message('gpg queue job timed out {}'.format(str(job_exception)))
                result_code = gpg_constants.TIMED_OUT_RESULT

            return result_code, gpg_output, gpg_error

        result_code = gpg_constants.ERROR_RESULT
        gpg_output = gpg_error = self.job = None
        try:
            current_job_timeout = self.DEFAULT_TIMEOUT
            if data is not None:
                data_length = len(data)
                self.log_message('data length: {}'.format(data_length))
                if data_length > gpg_constants.LARGE_DATA_CHUNK:
                    # set the timeout if the data is large; the timeout
                    # should be a little larger than the gpg timeout
                    current_job_timeout += int(
                      (data_length / gpg_constants.LARGE_DATA_CHUNK) * gpg_constants.TIMEOUT_PER_CHUNK)

            # arguments for the 'gpg' command itself
            gpg_command_args = [self.get_home_dir(), initial_args, passphrase, data]

            self.queue_connection = Redis(REDIS_HOST, self.get_queue_port())
            self.queue = Queue(name=GPG_RQ, connection=self.queue_connection)
            self.log_message('jobs waiting in gpg queue {}'.format(self.queue.count))
            self.log_message('about to queue {}'.format(command))

            # each job needs to wait for the jobs ahead of it so when
            # calculating the timeout include the jobs already in the queue
            timeout = current_job_timeout * (self.queue.count + 1)
            self.log_message('{} job timeout in seconds: {}'.format(command, timeout))
            self.job = self.queue.enqueue_call(execute_gpg_command, args=gpg_command_args, timeout=timeout)

            if self.job is None:
                self.log_message('unable to queue {} job'.format(command))
            else:
                job_id = self.job.get_id()

                self.log_message('{} job: {} times out in {} secs'.format(command, job_id, self.job.timeout))
                wait_until_queued(self.queue.count)

                self.log_message('wait for {} job results: {}'.format(command, wait_for_results))
                if wait_for_results:
                    result_code, gpg_output, gpg_error = wait_for_job(command)
                else:
                    if self.job.is_failed:
                        result_code = gpg_constants.ERROR_RESULT
                        job_dump = self.job.to_dict()
                        if 'exc_info' in job_dump:
                            gpg_error = job_dump['exc_info']
                            log_message('{} job exc info: {}'.format(job_id, error))
                        elif 'status' in job_dump:
                            gpg_error = job_dump['status']
                            self.log_message('{} job status: {}'.format(job_id, gpg_error))
                        self.log_message('job dump:\n{}'.format(job_dump))
                        self.job.cancel()
                        self.queue.remove(job_id)

                    elif self.job.is_queued or self.job.is_started or self.job.is_finished:
                        result_code = gpg_constants.GOOD_RESULT
                        self.log_message('{} queued {} job'.format(self.queue, job_id))

                    else:
                        self.log_message('{} {} job results: {}'.format(
                          self.queue, job_id, self.job.result))

        except Exception as exception:
            gpg_error = str(exception)
            record_exception()
            self.log_message('EXCEPTION - see syr.exception.log for details')

        if isinstance(result_code, bool):
            if result_code:
                result_code = gpg_constants.GOOD_RESULT
            else:
                result_code = gpg_constants.ERROR_RESULT

        return result_code, gpg_output, gpg_error

    def is_good_result(self):
        '''
            Return if the exit code is a good result
        '''

        return gpg_constants.GOOD_RESULT

    def is_error_result(self):
        '''
            Return if the exit code is an error
        '''

        return gpg_constants.ERROR_RESULT

    def is_timedout_result(self):
        '''
            Return if job timed out
        '''

        return gpg_constants.TIMED_OUT_RESULT

    def log_data(self, data, message="data"):
        ''' Log data. '''

        if self.DEBUGGING:
            self.log_message("{}:\n{}".format(message, data))

    def log_message(self, message):
        ''' Log a message. '''

        if self.log is None:
            self.log = LogFile()

        self.log.write_and_flush(message)

    def _sign_and_encrypt_now(self, data, from_user_id, to_user_id, passphrase, clear_sign, args):
        '''
            Sign and encrypt the data now. Only used internally. Use one of the public
            functions to sign, encrypt, and armor data, and finally clear sign it.
        '''

        encrypted_data = error_message = None

        try:
            if data is None:
                error_message = 'Could not sign and encrypt because there is no data.'
                self.log_message(error_message)
            elif from_user_id is None:
                error_message = 'Could not sign and encrypt because there is no FROM user id.'
                self.log_message(error_message)
            elif to_user_id is None:
                error_message = 'Could not sign and encrypt because there is no TO user id.'
                self.log_message(error_message)
            elif passphrase is None:
                error_message = 'Could not sign and encrypt because there is no passphrase.'
                self.log_message(error_message)
            else:
                self.log_data(data)

                result_code, gpg_output, gpg_error = self.gpg_command(
                    args, passphrase=passphrase, data=data, wait_for_results=True)
                self.log_message('results after signing, encrypting, and armoring: {}'.format(result_code))
                if result_code == gpg_constants.GOOD_RESULT:
                    if clear_sign:
                        encrypted_data, error_message = self.sign(gpg_output, from_user_id, passphrase)
                        self.log_message('results after clear signing: {}'.format(len(encrypted_data) > 0))
                        if error_message is not None:
                            self.log_message(error_message)
                    else:
                        encrypted_data = gpg_output
                else:
                    if gpg_error:
                        self.log_message(gpg_error)
                        error_message = gpg_error
                    if gpg_output:
                        self.log_message(gpg_output)
                        if error_message is None:
                            error_message = gpg_output

                    if error_message is not None and 'using subkey' in error_message:
                        i = error_message.find('\n')
                        if i > 0:
                            error_message = error_message[i:]
        except TypeError as type_error:
            self.handle_unexpected_exception(type_error)
            if error_message is None:
                error_message = 'Unexecpted type error'
        except Exception as exception:
            self.handle_unexpected_exception(exception)
            if error_message is None:
                error_message = 'Unexecpted exception'

        return encrypted_data, error_message


    def _parse_signer(self, output_string):
        ''' Parse the signer from the gpg command's results. '''

        signer = None

        try:
            if output_string is not None:
                lines = output_string.split('\n')
                for line in lines:
                    if line.startswith(self.GOOD_SIGNATURE_PREFIX):
                        signer = line[len(self.GOOD_SIGNATURE_PREFIX):].strip('"')
                        if len(signer) > 0:
                            break

                    # gpg reports the sig is bad if the key is a multi-email key and
                    # the first key doesn't match the user that created the signature
                    elif line.startswith(self.BAD_SIGNATURE_PREFIX):
                        signer = line[len(self.BAD_SIGNATURE_PREFIX):].strip('"')
                        if len(signer) > 0:
                            break

            # strip the extra comment (e.g., [ultimate])
            if signer is not None:
                i = signer.rfind('"')
                if i > 0:
                    signer = signer[:i]
                signer = signer.strip()
                signer = signer.strip('"')
        except Exception as exception:
            self.handle_unexpected_exception(exception)

        return signer


    def _parse_version(self, version):
        ''' Parse the version from the gpg command's results. '''

        version_number = None

        if version is None:
            version_number = ''
        else:
            reader = StringIO(initial_value=version)
            for l in reader:
                line = l.strip()
                index = line.rfind(' ')
                if index >= 0:
                    possibleVersionNumber = line[index + 1:]

                    #  make sure we got something resembling "X.Y"
                    dotIndex = possibleVersionNumber.find('.')
                    if dotIndex > 0 and dotIndex < len(possibleVersionNumber) - 1:
                        version_number = possibleVersionNumber
                    else:
                        self.log_message("version number not found in " + line)

                #  stop looking when we find the version
                if version_number != None:
                    break

        if version_number == None:
            version_number = ''

        return version_number

    def _create_basic_key_files(self):

        def create_key_file(filename):
            fullname = os.path.join(self.gpg_home, filename)
            sh.touch(fullname)
            os.chmod(fullname, 0o600)
            self.log_message('{} exists: {}'.format(filename, os.path.exists(filename)))

        create_key_file(gpg_constants.PUBLIC_KEY_FILENAME)
        create_key_file(gpg_constants.SECRET_KEY_FILENAME)
        create_key_file(gpg_constants.TRUST_DB_FILENAME)
Ejemplo n.º 3
0
class GPGExec(object):
    '''
        Execute a gpg command.

        gpg expects single tasks so we use redis to queue tasks.

        -fd: 0 = stdin
             1 = stdout
             2 = stderr
    '''

    DEBUGGING = False

    def __init__(self, home_dir, auto_check_trustdb=False):
        '''
            Create a new GPGExec object.

            >>> gpg_exec = GPGExec('/var/local/projects/goodcrypto/server/data/oce/.gnupg')
            >>> gpg_exec != None
            True
        '''

        self.log = LogFile()

        self.gpg_home = home_dir

        self.result_code = gpg_constants.ERROR_RESULT
        self.gpg_output = None
        self.gpg_error = None

        self.set_up_conf()

        # --no-tty: Do not write anything to TTY
        # --homedir: home directory for gpg's keyring files
        # --verbose: give details if error
        # --ignore-time-conflict: Since different machines have different ideas of what time it is, we want to ignore time conflicts.
        # --ignore-valid-from: "valid-from" is just a different kind of time conflict.
        # --batch: We're always in batch mode.
        # --lock-once: Lock the databases the first time a lock is requested and do not release the lock until the process terminates.
        # --no-auto-key-locate: Don't look for keys outside our system
        # --no-auto-check-trustdb: Do not always update the trust db because it goes online
        # --always-trust: We don't have any trust infrastructure yet.
        # --utf8-strings: Assume all arguments are in UTF-8 format.
        # redirect stdout and stderr so we can exam the results as needed
        kwargs = dict(no_tty=True, verbose=True, homedir=self.gpg_home,
           ignore_time_conflict=True, ignore_valid_from=True, batch=True,
           no_auto_key_locate=True, lock_once=True, utf8_strings=True, _env=minimal_env())

        # gpg tries to go online when it updates the trustdb
        # so we don't want to check on every command
        if auto_check_trustdb:
            kwargs['auto_check_trustdb'] = True
            self.log_message('auto_check_trustdb')
        else:
            kwargs['no_auto_check_trustdb'] = True
            self.log_message('no_auto_check_trustdb')
            if (self.gpg_home is not None and
                os.path.exists(os.path.join(self.gpg_home, gpg_constants.TRUST_DB_FILENAME))):
                kwargs['always_trust'] = True
        self.gpg = sh.gpg.bake(**kwargs)

        # make sure no old job has left locked files
        self.clear_gpg_lock_files()
        self.clear_gpg_tmp_files()


    def execute(self, initial_args, passphrase=None, data=None):
        ''' Prepare and then run a gpg command. '''

        result_ok = False
        timeout = None
        try:
            stdin_file = StringIO()
            args = initial_args
            if GPGExec.DEBUGGING: self.log_message("executing: {}".format(args))

            if passphrase and len(passphrase) > 0:
                self.log_message('passphrase supplied')

                # passphrase will be passed on stdin, file descriptor 0 is stdin
                passphraseOptions = ['--passphrase-fd', '0']
                args.append(passphraseOptions)
                stdin_file.write(passphrase)
                stdin_file.write(gpg_constants.EOL)

            if data:
                if isinstance(data, bytearray) or isinstance(data, bytes):
                    data = data.decode()
                stdin_file.write(data)
                data_length = len(data)
                self.log_message('data length: {}'.format(data_length))
                if data_length > gpg_constants.LARGE_DATA_CHUNK:
                    timeout = int(
                      (data_length / gpg_constants.LARGE_DATA_CHUNK) * gpg_constants.TIMEOUT_PER_CHUNK) *  1000 # in ms
                    self.log_message('timeout in ms: {}'.format(data_length))

                if GPGExec.DEBUGGING: self.log_message("data: {}".format(data))

            stdin = stdin_file.getvalue()
            stdin_file.close()

            if GPGExec.DEBUGGING:
                self.log_message("gpg args:")
                for arg in args:
                    self.log_message('  {}'.format(arg))

            result_ok = self.run_gpg(args, stdin, timeout=timeout)
            self.log_message("gpg command result_ok: {}".format(result_ok))

        except Exception as exception:
            result_ok = False
            self.result_code = gpg_constants.ERROR_RESULT
            self.gpg_error = str(exception)

            self.log_message('result code: {}'.format(self.result_code))
            if self.gpg_error and len(self.gpg_error.strip()) > 0:
                self.log_message("gpg error: {}".format(self.gpg_error))
            self.log_message('EXCEPTION - see syr.exception.log for details')
            record_exception()

        self.log.flush()

        return self.result_code, self.gpg_output, self.gpg_error

    def run_gpg(self, args, stdin, timeout=None):
        ''' Run the GPG command. '''

        try:
            self.gpg_output = self.gpg_error = None
            if gpg_constants.ENCRYPT_DATA in args:
                command = gpg_constants.ENCRYPT_DATA
            elif gpg_constants.DECRYPT_DATA in args:
                command = gpg_constants.DECRYPT_DATA
            elif gpg_constants.SEARCH_KEYSERVER in args:
                command = gpg_constants.SEARCH_KEYSERVER
            elif gpg_constants.RETRIEVE_KEYS in args:
                command = gpg_constants.RETRIEVE_KEYS
            else:
                command = args[0]
            self.log_message('--- started executing: {} ---'.format(command))

            with elapsed_time() as gpg_time:
                if timeout is None:
                    if stdin and len(stdin) > 0:
                        gpg_results = self.gpg(*args, _in=stdin, _ok_code=[0,2])
                    else:
                        gpg_results = self.gpg(*args, _ok_code=[0,2])
                else:
                    if stdin and len(stdin) > 0:
                        gpg_results = self.gpg(*args, _in=stdin, _ok_code=[0,2], _timeout=timeout)
                    else:
                        gpg_results = self.gpg(*args, _ok_code=[0,2], _timeout=timeout)

                self.log_message('{} command elapsed time: {}'.format(command, gpg_time))

            self.log_message('{} exit code: {}'.format(command, gpg_results.exit_code))
            self.log_message('--- finished executing: {} ---'.format(command))

            self.result_code = gpg_results.exit_code
            self.gpg_output = self.get_decoded_results(gpg_results.stdout)
            self.gpg_error = self.get_decoded_results(gpg_results.stderr)

            if GPGExec.DEBUGGING:
                if self.gpg_output:
                    self.log_message('stdout: {}'.format(self.gpg_output))
                if self.gpg_error:
                    self.log_message('stderr:{}'.format(self.gpg_error))

        except sh.ErrorReturnCode as exception:
            self.result_code = exception.exit_code

            if self.gpg_error is None:
                self.gpg_error = exception.stderr

            # get the essence of the error
            self.gpg_error = exception.stderr
            if self.gpg_error and self.gpg_error.find(':'):
                self.gpg_error = self.gpg_error[self.gpg_error.find(':') + 1:]
            if self.gpg_error and self.gpg_error.find(':'):
                self.gpg_error = self.gpg_error[self.gpg_error.find(':') + 1:]

            self.log_message('exception result code: {}'.format(self.result_code))
            if exception:
                self.log_message("exception:\n==============\n{}\n============".format(exception))

        except JobTimeoutException as job_exception:
            self.log_message('run_gpg exception: {}'.format(str(job_exception)))
            self.result_code = gpg_constants.TIMED_OUT_RESULT
            self.gpg_error = str(job_exception)
            self.gpg_output = None

            self.log_message('--- timedout executing {} ---'.format(command))

        return (self.result_code == gpg_constants.GOOD_RESULT or
                self.result_code == gpg_constants.CONDITIONAL_RESULT)

    def get_decoded_results(self, results):
        '''
            Get the decoded results.

            >>> exec = GPGExec('/var/local/projects/goodcrypto/server/data/oce/.gnupg')
            >>> exec.get_decoded_results(b'test')
            'test'
        '''

        def decode_with(results, charset):
            try:
                decoded_results = results.decode(encoding=charset)
            except ValueError as ve:
                decoded_results = None

            return decoded_results

        decoded_results = decode_with(results, 'utf-8')
        if decoded_results is None:
            decoded_results = decode_with(results, 'iso-8859-1')
        if decoded_results is None:
            decoded_results = decode_with(results, 'iso-8859-2')
        if decoded_results is None:
            decoded_results = decode_with(results, 'iso-8859-15')
        if decoded_results is None:
            decoded_results = decode_with(results, 'koi8-r')
        if decoded_results is None:
            decoded_results = results
            self.log_message('unable to decode results "{}" with standard gpg charsets'.format(results))

        return decoded_results

    def set_up_conf(self):
        ''' Set up the GPG conf file, if it doesn't exist. '''

        try:
            if self.gpg_home is None:
                self.log_message('gpg home not defined yet')
            else:
                gpg_conf = os.path.join(self.gpg_home, gpg_constants.CONF_FILENAME)
                if not os.path.exists(gpg_conf):
                    lines = []
                    lines.append('#\n')
                    lines.append('# This is an adpation of the Riseup OpenPGP Best Practices\n')
                    lines.append('# https://help.riseup.net/en/security/message-security/openpgp/best-practices\n')
                    lines.append('#\n')
                    lines.append('#-----------------------------\n')
                    lines.append('# behavior\n')
                    lines.append('#-----------------------------\n')
                    lines.append('# Disable inclusion of the version string in ASCII armored output\n')
                    lines.append('no-emit-version\n')
                    lines.append('# Disable comment string in clear text signatures and ASCII armored messages\n')
                    lines.append('no-comments\n')
                    lines.append('# Display long key IDs\n')
                    lines.append('keyid-format 0xlong\n')
                    lines.append('# List all keys (or the specified ones) along with their fingerprints\n')
                    lines.append('with-fingerprint\n')
                    lines.append('# Display the calculated validity of user IDs during key listings\n')
                    lines.append('list-options show-uid-validity\n')
                    lines.append('verify-options show-uid-validity\n')
                    lines.append('# Try to use the GnuPG-Agent. With this option, GnuPG first tries to connect to\n')
                    lines.append('# the agent before it asks for a passphrase.\n')
                    lines.append('use-agent\n')
                    lines.append('#-----------------------------\n')
                    lines.append('# keyserver -- goodcrypto relies on per-to-per key exchange, not key servers\n')
                    lines.append('#-----------------------------\n')
                    lines.append('# This is the server that --recv-keys, --send-keys, and --search-keys will\n')
                    lines.append('# communicate with to receive keys from, send keys to, and search for keys on\n')
                    lines.append('# keyserver hkps://hkps.pool.sks-keyservers.net\n')
                    lines.append('# Provide a certificate store to override the system default\n')
                    lines.append('# Get this from https://sks-keyservers.net/sks-keyservers.netCA.pem\n')
                    lines.append('# keyserver-options ca-cert-file=/usr/local/etc/ssl/certs/hkps.pool.sks-keyservers.net.pem\n')
                    lines.append('# Set the proxy to use for HTTP and HKP keyservers - default to the standard\n')
                    lines.append('# local Tor socks proxy\n')
                    lines.append('# It is encouraged to use Tor for improved anonymity. Preferrably use either a\n')
                    lines.append('# dedicated SOCKSPort for GnuPG and/or enable IsolateDestPort and\n')
                    lines.append('# IsolateDestAddr\n')
                    lines.append('#keyserver-options http-proxy=socks5-hostname://127.0.0.1:9050\n')
                    lines.append("# Don't leak DNS, see https://trac.torproject.org/projects/tor/ticket/2846\n")
                    lines.append('keyserver-options no-try-dns-srv\n')
                    lines.append('# When using --refresh-keys, if the key in question has a preferred keyserver\n')
                    lines.append('# URL, then disable use of that preferred keyserver to refresh the key from\n')
                    lines.append('keyserver-options no-honor-keyserver-url\n')
                    lines.append('# When searching for a key with --search-keys, include keys that are marked on\n')
                    lines.append('# the keyserver as revoked\n')
                    lines.append('keyserver-options include-revoked\n')
                    lines.append('#-----------------------------\n')
                    lines.append('# algorithm and ciphers\n')
                    lines.append('#-----------------------------\n')
                    lines.append('# list of personal digest preferences. When multiple digests are supported by\n')
                    lines.append('# all recipients, choose the strongest one\n')
                    lines.append('personal-cipher-preferences AES256 AES192 AES CAST5\n')
                    lines.append('# list of personal digest preferences. When multiple ciphers are supported by\n')
                    lines.append('# all recipients, choose the strongest one\n')
                    lines.append('personal-digest-preferences SHA512 SHA384 SHA256 SHA224\n')
                    lines.append('# message digest algorithm used when signing a key\n')
                    lines.append('cert-digest-algo SHA512\n')
                    lines.append('# This preference list is used for new keys and becomes the default for\n')
                    lines.append('# "setpref" in the edit menu\n')
                    lines.append('default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed\n')
                    '''
                    lines.append('# when outputting certificates, view user IDs distinctly from keys:\n')
                    lines.append('fixed-list-mode\n')
                    lines.append("# long keyids are more collision-resistant than short keyids (it's trivial to make a key with any desired short keyid)")
                    lines.append('keyid-format 0xlong\n')
                    lines.append('# when multiple digests are supported by all recipients, choose the strongest one:\n')
                    lines.append('personal-digest-preferences SHA512 SHA384 SHA256 SHA224\n')
                    lines.append('# preferences chosen for new keys should prioritize stronger algorithms: \n')
                    lines.append('default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 BZIP2 ZLIB ZIP Uncompressed\n')
                    lines.append("# If you use a graphical environment (and even if you don't) you should be using an agent:")
                    lines.append('# (similar arguments as  https://www.debian-administration.org/users/dkg/weblog/64)\n')
                    lines.append('use-agent\n')
                    lines.append('# You should always know at a glance which User IDs gpg thinks are legitimately bound to the keys in your keyring:\n')
                    lines.append('verify-options show-uid-validity\n')
                    lines.append('list-options show-uid-validity\n')
                    lines.append('# include an unambiguous indicator of which key made a signature:\n')
                    lines.append('# (see http://thread.gmane.org/gmane.mail.notmuch.general/3721/focus=7234)\n')
                    lines.append('sig-notation [email protected]=%g\n')
                    lines.append('# when making an OpenPGP certification, use a stronger digest than the default SHA1:\n')
                    lines.append('cert-digest-algo SHA256\n')
                    '''

                    self.log_message('creating {}'.format(gpg_conf))
                    with open(gpg_conf, 'wt') as f:
                        for line in lines:
                            f.write(line)
                    sh.chmod('0400', gpg_conf)
                    self.log_message('created {}'.format(gpg_conf))
        except Exception:
            record_exception()
            self.log_message('EXCEPTION - see syr.exception.log for details')

    def clear_gpg_lock_files(self):
        '''
            Delete gpg lock files.

            Warning: Calling this method when a valid lock file exists can have very
            serious consequences.

            Lock files are in gpg home directory and are in the form
            ".*.lock", ".?*", or possibly "trustdb.gpg.lock".
        '''

        try:
            if self.gpg_home is None:
                self.log_message("unable to clear gpg's lock files because home dir unknown")
            else:
                filenames = os.listdir(self.gpg_home)
                if filenames and len(filenames) > 0:
                    for name in filenames:
                        if name.endswith(gpg_constants.LOCK_FILE_SUFFIX):
                            os.remove(os.path.join(self.gpg_home, name))
                            self.log_message("deleted lock file " + name)
        except Exception:
            record_exception()
            self.log_message('EXCEPTION - see syr.exception.log for details')

    def clear_gpg_tmp_files(self):
        '''
            Delete gpg tmp files.
        '''

        TmpPREFIX = 'tmp'
        TmpSUFFIX = '~'

        try:
            if self.gpg_home is None:
                self.log_message("unable to clear gpg's tmp files because home dir unknown")
            else:
                filenames = os.listdir(self.gpg_home)
                if filenames and len(filenames) > 0:
                    for name in filenames:
                        if name.startswith(TmpPREFIX) and name.endswith(TmpSUFFIX):
                            os.remove(os.path.join(self.gpg_home, name))
        except Exception:
            record_exception()
            self.log_message('EXCEPTION - see syr.exception.log for details')

    def log_message(self, message):
        '''
            Log the message.
        '''

        if self.log is None:
            self.log = LogFile()

        self.log.write_and_flush(message)