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
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)
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)