def get_hashcode(message): ''' Gets a hashcode for a message. >>> from goodcrypto_tests.mail import message_utils >>> filename = message_utils.get_plain_message_name('basic.txt') >>> with open(filename) as input_file: ... get_hashcode(input_file) is not None True ''' hash_code = None try: from goodcrypto.mail.message.email_message import EmailMessage email_message = EmailMessage(message) message_string = email_message.to_string() if message_string == None: if isinstance(get_last_error(), Exception): raise MessageException("Invalid message") else: raise MessageException("Invalid message: {}".format(get_last_error())) else: charset, __ = get_charset(email_message) hash_code = sha224(bytearray(message_string, charset)).hexdigest().upper() except Exception as hash_exception: set_last_error(hash_exception) record_exception() return hash_code
def otp_filename(mailfrom, mailto): ''' Returns One Time Pad filename for email_addr. If there is no otp for email_addr, starts process to create one. ''' # just use the email address part, the string with '@' __, mailfrom = email.utils.parseaddr(mailfrom) __, mailto = email.utils.parseaddr(mailto) users_dir = 'something unknown' user_dir = os.path.join(users_dir, mailfrom) if not os.path.exists(user_dir): raise MessageException('No directory for user: {0}'.format(mailfrom)) peer_dir = os.path.join(users_dir, mailto) if not os.path.exists(peer_dir): raise MessageException('No directory for user, peer: {0}, {1}'.format( mailfrom, mailto)) filename = os.path.join(users_dir, 'otp') if not os.path.exists(filename): thread = Thread(target=create_otp, args=(filename)) thread.start() raise MessageException( 'Creating otp file for user, peer: {0}, {1}'.format( mailfrom, mailto)) return filename
def decrypt_message(self): ''' Decrypt a message for the original recipient. ''' try: self.log_message('decrypting message') if self.DEBUGGING and self.crypto_message is not None: self.log_message( 'original message:\n{}'.format(self.crypto_message.get_email_message().to_string())) decrypt = Decrypt(self.crypto_message) self.crypto_message = decrypt.process_message() self.log_message('decrypted message: {}'.format(self.crypto_message.is_crypted())) # send our metadata key to the recipient if needed if decrypt.needs_metadata_key(): self.log_message('need to send metadata key') metadata_key_sent = send_metadata_key( self.crypto_message.smtp_sender(), self.crypto_message.smtp_recipient()) self.log_message('sent metadata key: {}'.format(metadata_key_sent)) except CryptoException as crypto_exception: raise MessageException(value=crypto_exception.value) except MessageException as message_exception: raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') try: self.crypto_message.add_error_tag_once(SERIOUS_ERROR_PREFIX) except Exception: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') raise MessageException()
def process_message(self): ''' If the message is an encrypted message to a single individual, then decrypt the message and send it to the recipient. If the message is an encrypted message that protects metadata and contains one or more messages inside, then decrypt the metadata, decrypt the inner message(s) and deliver each message to the intended recipient. If the message is not encrypted, then just add a warning about receiving unencrypted messages. See unittests for usage as the test set up is too complex for a doctest. ''' try: filtered = decrypted = dkim_sig_verified = False from_user = self.crypto_message.smtp_sender() to_user = self.crypto_message.smtp_recipient() # see if there's a metadata wrapper if is_metadata_address(from_user) and is_metadata_address(to_user): dkim_sig_verified = self.decrypt_metadata() # if only one of to/from is a metadata address, the message is bad elif is_metadata_address(from_user) or is_metadata_address(to_user): self.log_message("bad envelope: from user: {} to_user: {}".format(from_user, to_user)) self.crypto_message.drop() if not self.crypto_message.is_dropped(): # now see if there's a bundle of padded messages if (is_metadata_address(self.crypto_message.smtp_sender()) and is_metadata_address(self.crypto_message.smtp_recipient())): self.unbundle_wrapped_messages() else: if not dkim_sig_verified and options.verify_dkim_sig(): self.crypto_message, dkim_sig_verified = decrypt_utils.verify_dkim_sig( self.crypto_message) self.log_message('dkim signature must be verified: {}'.format(dkim_sig_verified)) self.decrypt_message() except CryptoException as crypto_exception: raise MessageException(value=crypto_exception.value) except MessageException as message_exception: raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') return self.crypto_message
def _create_good_message_from_bad(self, source): ''' Create a good message from a source that contains a corrupted message. >>> from goodcrypto_tests.mail.message_utils import get_plain_message_name >>> with open(get_plain_message_name('bad-basic.txt')) as input_file: ... email_message = EmailMessage() ... email_message._create_good_message_from_bad(input_file) ''' try: # start with a fresh message self._message = Message() if isinstance(source, IOBase): source.seek(os.SEEK_SET) message_string = source.read() else: message_string = source body_text = self._create_new_header(message_string) if body_text: self._create_new_body_text(body_text) except Exception as message_exception: self.log_message(message_exception) record_exception() raise MessageException(message_exception)
def encrypt_mime_message(crypto_message, crypto, users_dict): ''' Encrypt a MIME message by encrypting the entire original message and creating a new plain text message with the payload the encrypted original message. This reduces the metadata someone can collect, but it does require the receiving end decrypt the message and create a new readable message from the encrypted original message. ''' def copy_item_from_original(msg, keyword): value = crypto_message.get_email_message().get_header(keyword) if value is not None: msg.__setitem__(keyword, value) log_message('encrypting a mime message') message = crypto_message.get_email_message().get_message() log_message('content type: {}'.format(message.get_content_type())) # Encrypt the whole message and add it to the body text # This removes important meta data. The recieving end must # decrypt the message, and then create a new message with the original structure. email_message = crypto_message.get_email_message() charset, __ = get_charset(email_message) log_message('about to encrypt {} mime message'.format(charset)) ciphertext, error_message = encrypt_byte_array( bytearray(email_message.to_string(), charset), crypto, users_dict) if ciphertext is not None and len(ciphertext) > 0: from_user = users_dict[FROM_KEYWORD] convert_encrypted_mime_message(crypto_message, ciphertext, from_user, users_dict[TO_KEYWORD]) set_sigs(crypto_message, from_user, users_dict[PASSCODE_KEYWORD]) elif error_message is not None: raise MessageException(value=error_message)
def sign_mime_message(crypto_message, crypto, from_user_id, passcode): ''' Sign a MIME message by signing the entire original message and creating a new plain text message with the payload the signed original message. This reduces the metadata someone can collect, but it does require the receiving end decrypt the message and create a new readable message from the signed original message. ''' def copy_item_from_original(msg, keyword): value = crypto_message.get_email_message().get_header(keyword) if value is not None: msg.__setitem__(keyword, value) log_message('signing a mime message') message = crypto_message.get_email_message().get_message() log_message('content type: {}'.format(message.get_content_type())) # Sign the whole message and add it to the body text # This removes important meta data. The recieving end must # decrypt the message, and then create a new message with the original structure. log_message('about to sign mime message') email_message = crypto_message.get_email_message() charset, __ = get_charset(email_message) ciphertext, error_message = crypto.sign( bytearray(email_message.to_string(), charset), from_user_id, passcode) if ciphertext is not None and len(ciphertext) > 0: convert_encrypted_mime_message(crypto_message, ciphertext, from_user_id, crypto_message.smtp_recipient()) elif error_message is not None: raise MessageException(value=error_message)
def _encrypt_message_with_all(self, encryption_names): ''' Encrypt the message with each encryption program. ''' encrypted = fatal_error = False error_message = '' encrypted_with = [] encrypted_classnames = [] to_user = self.crypto_message.smtp_recipient() original_payload = self.crypto_message.get_email_message().get_message().get_payload() self._log_message("encrypting using {} with {}'s key".format(encryption_names, to_user)) for encryption_name in encryption_names: encryption_classname = get_key_classname(encryption_name) if encryption_classname not in encrypted_classnames: try: if options.require_key_verified(): __, key_ok, __ = contacts.get_fingerprint(to_user, encryption_name) self._log_message("{} {} key verified: {}".format(to_user, encryption_name, key_ok)) else: key_ok = True if key_ok: if self._encrypt_message(encryption_name): encrypted_classnames.append(encryption_classname) encrypted_with.append(encryption_name) else: error_message += i18n('You need to verify the {encryption} key for {email} before you can use it.'.format( encryption=encryption_name, email=to_user)) self._log_message(error_message) except MessageException as message_exception: fatal_error = True error_message += message_exception.value self._log_exception(error_message) break # if the user has encryption software defined, then the message # must be encrypted or bounced to the sender if len(encrypted_classnames) > 0: encrypted = True else: MSG_NOT_SET = i18n('Message not sent to {email} because there was a problem encrypting.'.format( email=to_user)) fatal_error = True if error_message is None or len(error_message) <= 0: error_message = '{} {}\n{}'.format(MSG_NOT_SET, i18n("It's possible the recipient's key is missing."), self.POSSIBLE_ENCRYPT_SOLUTION) else: error_message = '{} {}'.format(MSG_NOT_SET, error_message) # restore the payload self.crypto_message.get_email_message().get_message().set_payload(original_payload) if fatal_error: self._log_message('raising message exception in _encrypt_message_with_all') self._log_message(error_message) raise MessageException(value=error_message) return encrypted_with
def split_and_send_messages(self, message): ''' Split a message apart and send each to the intended recipient. ''' result_ok = False self.messages_sent = 0 try: for part in message.walk(): try: if part.get_content_type() == mime_constants.APPLICATION_ALT_TYPE: content = b64decode(part.get_payload(decode=True)) inner_crypto_message = self.create_inner_message(content) if inner_crypto_message is not None: self.log_message('logged inner message headers in goodcrypto.message.utils.log') utils.log_message_headers(inner_crypto_message, tag='inner message headers') ok, __ = self.decrypt_and_send_message(inner_crypto_message) if ok: self.messages_sent += 1 except CryptoException as crypto_exception: raise MessageException(value=crypto_exception.value) except MessageException as message_exception: raise MessageException(value=message_exception.value) except: self.log_message('bad part of message discarded') self.log_message('EXCEPTION - see syr.exception.log for details') record_exception() result_ok = True self.log_message('good bundled message contains {} inner message(s)'.format(self.messages_sent)) except CryptoException as crypto_exception: raise MessageException(value=crypto_exception.value) except MessageException as message_exception: raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') result_ok = False return result_ok
def _get_recipient_encryption_software(self): ''' Get the software to decrypt a message for the recipient (internal use only). If the user doesn't have a key, then configure one and return None. ''' encryption_software = None if self.crypto_message is None: self.log_message("missing crypto_message".format( self.crypto_message)) else: try: from_user = self.crypto_message.smtp_sender() to_user = self.crypto_message.smtp_recipient() encryption_software = contacts.get_encryption_names(to_user) if len(encryption_software) > 0: self.log_message( "encryption software: {}".format(encryption_software)) elif email_in_domain( to_user) and options.create_private_keys(): add_private_key(to_user, encryption_software=encryption_software) self.log_message( "started to create a new {} key for {}".format( encryption_software, to_user)) else: self.log_message( "no encryption software for {}".format(to_user)) self.crypto_message.add_error_tag_once( i18n( 'Message appears encrypted, but {email} does not use any known encryption' .format(email=to_user))) report_unable_to_decrypt( to_user, self.crypto_message.get_email_message().to_string()) except CryptoException as crypto_exception: raise CryptoException(crypto_exception.value) except Exception as IOError: utils.log_crypto_exception(MessageException(format_exc())) record_exception() self.log_message( 'EXCEPTION - see syr.exception.log for details') try: self.crypto_message.add_error_tag_once( SERIOUS_ERROR_PREFIX) except Exception: record_exception() self.log_message( 'EXCEPTION - see syr.exception.log for details') return encryption_software
def _process_no_metadata_message(self, to_user): ''' Handle a message without metadata protection. ''' if self.crypto_message.is_crypted(): if self.DEBUGGING: self._log_message('full encrypted message:\n{}'.format( self.crypto_message.get_email_message().to_string())) elif contacts.never_encrypt_outbound(to_user): self._log_message('{} not using encryption'.format(to_user)) self.crypto_message.add_error_tag_once(USE_ENCRYPTION_WARNING) elif options.require_outbound_encryption(): self._log_message('message not sent because global encryption required') raise MessageException(value=self.MUST_ENCRYPT_ALL_MAIL.format(to_email=to_user)) elif contacts.always_encrypt_outbound(to_user): self._log_message('message not sent because encryption required for {}'.format(to_user)) raise MessageException(value=self.MUST_ENCRYPT_MAIL_TO_USER.format(to_email=to_user)) else: self.crypto_message.add_error_tag_once(USE_ENCRYPTION_WARNING)
def _process_plain_message(self, from_user, to_user, never_encrypt, encryption_names): ''' Handle the initial processing of a message that does not need encryption. ''' self._log_message('{} uses {} known encryption'.format(to_user, len(encryption_names))) if not self.ready_to_protect_metadata: if never_encrypt: self._log_message('{} set not to use any encryption'.format(to_user)) # fail if encryption is required globally or for this individual elif options.require_outbound_encryption(): self._log_message('message not sent because global encryption required') raise MessageException(value=self.MUST_ENCRYPT_ALL_MAIL.format(to_email=to_user)) elif contacts.always_encrypt_outbound(to_user): self._log_message('message not sent because encryption required for {}'.format(to_user)) raise MessageException(value=self.MUST_ENCRYPT_MAIL_TO_USER.format(to_email=to_user)) # see if message must be clear signed if options.clear_sign_email(): self._clear_sign_crypto_message(from_user) # add the public key so the receiver can use crypto with us in the future if options.auto_exchange_keys(): self.crypto_message.add_public_key_to_header(from_user) self._log_message('added {} public key to header'.format(from_user)) # if we're not exchanging keys, but we are creating them, # then start the process in background elif options.create_private_keys(): add_private_key(from_user) self._log_message('created private key for {} if needed'.format(from_user)) self.crypto_message.set_filtered(True)
def _manage_key_header(self, from_user, crypto_message, encryption_name, key_block): ''' Manage a key in the header for the encryption software (internal use only). ''' tag = None try: if key_block == None or len(key_block.strip()) <= 0: self.log_message( "no {} public key in header".format(encryption_name)) else: if self.DEBUGGING: self.log_message("{} key from message:\n{}".format( encryption_name, key_block)) key_crypto = KeyFactory.get_crypto( encryption_name, crypto_software.get_key_classname(encryption_name)) if key_crypto is None: id_fingerprint_pairs = None self.log_message( 'no key crypto for {}'.format(encryption_name)) else: id_fingerprint_pairs = key_crypto.get_id_fingerprint_pairs( key_block) if id_fingerprint_pairs is None or len( id_fingerprint_pairs) <= 0: tag = None self.log_message('no user keys in key block') else: tag = self._manage_public_key(from_user, crypto_message, key_crypto, key_block, id_fingerprint_pairs) if tag is not None and len(tag.strip()) > 0: crypto_message.add_tag_once(tag) except MessageException as message_exception: raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') return tag
def sign_text_message(crypto_message, crypto, from_user_id, passcode): ''' Sign a plain text message. ''' def sign_text_part(content, charset, crypto, from_user_id, passcode): if DEBUGGING: log_message('type of content: {}'.format(type(content))) ciphertext, error_message = crypto.sign(bytearray(content, charset), from_user_id, passcode) # if we signed successfully, save the results if ciphertext != None and len(ciphertext) > 0: crypto_message.get_email_message().get_message().set_payload( ciphertext) result_ok = True else: result_ok = False return result_ok, error_message log_message('signing a text message') error_message = None email_message = crypto_message.get_email_message() if is_multipart_message(email_message): for part in email_message.walk(): charset, __ = get_charset(part) result_ok, error_message = sign_text_part(part.get_payload(), charset, crypto, from_user_id, passcode) if not result_ok: break else: final_content = email_message.get_content() if DEBUGGING: log_message(' content:\n{!s}'.format(final_content)) charset, __ = get_charset(final_content) result_ok, error_message = sign_text_part(final_content, charset, crypto, from_user_id, passcode) # if we signed successfully, save the results if result_ok: crypto_message.set_filtered(True) crypto_message.set_crypted(True) elif error_message is not None: raise MessageException(value=error_message)
def _protect_metadata(self, from_user, to_user, inner_encrypted_with): ''' Protect the message's metadata and resist traffic analysis. ''' if options.bundle_and_pad(): tags_added = add_encrypted_tags_to_message(self.crypto_message) self._log_message('tags added to encrypted message before bundling: {}'.format(tags_added)) # add the DKIM signature if user opted for it self.crypto_message = encrypt_utils.add_dkim_sig_optionally(self.crypto_message) message_name = packetize( self.crypto_message, inner_encrypted_with, self.verification_code) if os.stat(message_name).st_size > options.bundled_message_max_size(): self._log_message('Message too large to bundle so throwing MessageException') if os.path.exists(message_name): os.remove(message_name) error_message = self.MESSAGE_TOO_LARGE.format(kb_size=options.bundle_message_kb()) self._log_message(error_message) raise MessageException(value=error_message) else: self._log_message('message waiting for bundling: {}'.format( self.crypto_message.is_processed())) else: self._log_message('protecting metadata') # add the original sender and recipient in the header self.crypto_message.get_email_message().add_header(constants.ORIGINAL_FROM, from_user) self.crypto_message.get_email_message().add_header(constants.ORIGINAL_TO, to_user) # add the DKIM signature to the inner message if user opted for it self.crypto_message = encrypt_utils.add_dkim_sig_optionally(self.crypto_message) self._log_message('DEBUG: logged headers before encrypting with metadata key in goodcrypto.message.utils.log') log_message_headers(self.crypto_message, tag='headers before encrypting with metadata key') # even if the inner message isn't encrypted, encrypt # the entire message to protect the metadata message_id = get_message_id(self.crypto_message.get_email_message()) self.crypto_message = encrypt_utils.create_protected_message( self.crypto_message.smtp_sender(), self.crypto_message.smtp_recipient(), self.crypto_message.get_email_message().to_string(), message_id) self._log_message('added protective layer for metadata')
def packetize(crypto_message, encrypted_with, verification_code): ''' Packetize for later delivery. ''' try: message_name = None domain = parse_domain(crypto_message.smtp_recipient()) dirname = os.path.join(get_packet_directory(), '.{}'.format(domain)) if not os.path.exists(dirname): os.mkdir(dirname, SafeDirPermissions) log_message('created packet queue for {}'.format(domain)) crypto_message.set_processed(True) encrypted_names = '' if crypto_message.is_crypted(): for encrypted_name in encrypted_with: if len(encrypted_names) > 0: encrypted_names += ', ' encrypted_names += encrypted_name log_message('queued message encrypted with: {}'.format(encrypted_names)) message_name = get_unique_filename(dirname, constants.MESSAGE_PREFIX, constants.MESSAGE_SUFFIX) with open(message_name, 'wt') as f: f.write(crypto_message.get_email_message().to_string()) f.write('{}'.format(constants.START_ADDENDUM)) f.write('{}: {}\n'.format(mime_constants.FROM_KEYWORD, crypto_message.smtp_sender())) f.write('{}: {}\n'.format(mime_constants.TO_KEYWORD, crypto_message.smtp_recipient())) f.write('{}: {}\n'.format(constants.CRYPTED_KEYWORD, crypto_message.is_crypted())) f.write('{}: {}\n'.format(constants.CRYPTED_WITH_KEYWORD, encrypted_names)) f.write('{}: {}\n'.format(constants.PRIVATE_SIGNED_KEYWORD, crypto_message.is_private_signed())) f.write('{}: {}\n'.format(constants.CLEAR_SIGNED_KEYWORD, crypto_message.is_clear_signed())) f.write('{}: {}\n'.format(constants.DKIM_SIGNED_KEYWORD, crypto_message.is_dkim_signed())) f.write('{}: {}\n'.format(constants.VERIFICATION_KEYWORD, verification_code)) f.write('{}'.format(constants.END_ADDENDUM)) log_message('packetized message filename: {}'.format(os.path.basename(message_name))) except: message_name = None crypto_message.set_processed(False) error_message = i18n('Unable to packetize message due to an unexpected error.') log_message(error_message) log_message('EXCEPTION - see syr.exception.log for details') record_exception() raise MessageException(value=error_message) return message_name
def is_message_valid(self): ''' Returns true if the message is parsable, else false. If a MIME message is not parsable, you should still be able to process it. As we find different errors in messages, we should make sure this method catches them. >>> from goodcrypto_tests.mail.message_utils import get_basic_email_message >>> good_message = get_basic_email_message() >>> validator = Validator(good_message) >>> validator.is_message_valid() True ''' is_valid = False self.why = None if self.email_message is None: raise MessageException('Message is None') else: try: self.email_message.write_to(StringIO()) if Validator.DEBUGGING: self.log_message("message after check:\n{}".format( self.email_message.to_string())) self._check_content(self.email_message) is_valid = True except Exception: is_valid = False # we explicitly want to catch everything here, even NPE record_exception() self.log_message( 'EXCEPTION - see syr.exception.log for details') self.log_message('message is valid: {}'.format(is_valid)) return is_valid
def _get_crypto_details(self, encryption_name, user_ids): ''' Get the details needed to encrypt a message. ''' from_user_id = to_user_id = passcode = None ready_to_encrypt = user_ids is not None and len(user_ids) > 0 if ready_to_encrypt: to_user_id, ready_to_encrypt = self._get_to_crypto_details(encryption_name, user_ids) if ready_to_encrypt or options.auto_exchange_keys(): from_user_id, passcode, error_message = self._get_from_crypto_details(encryption_name, user_ids) if ready_to_encrypt and passcode is None and options.clear_sign_email(): if error_message is not None: error_message += i18n(' and clear signing is required.') if options.create_private_keys(): error_message += '\n\n{}'.format(i18n( "You should wait 5-10 minutes and try again. If that doesn't help, then contact your mail administrator.")) else: error_message += '\n\n{}'.format(i18n( 'Ask your mail administrator to create a private key for you.')) self._log_exception(error_message) raise MessageException(error_message) if self.crypto_message is None or self.crypto_message.get_email_message() is None: charset = 'UTF-8' else: charset, __ = get_charset(self.crypto_message.get_email_message().get_message()) self._log_message('char set in crypto message: {}'.format(charset)) users_dict = {encrypt_utils.TO_KEYWORD: to_user_id, encrypt_utils.FROM_KEYWORD: from_user_id, encrypt_utils.PASSCODE_KEYWORD: passcode, encrypt_utils.CHARSET_KEYWORD: charset} self._log_message('got crypto details and ready to encrypt: {}'.format(ready_to_encrypt)) return users_dict, ready_to_encrypt
def _process_encrypt_message(self, encryption_names): ''' Handle the initial processing of a message that needs encryption. ''' inner_encrypted_with = [] try: if self.DEBUGGING: self._log_message('message before encryption:\n{}'.format( self.crypto_message.get_email_message().to_string())) self._log_message('trying to encrypt using {}'.format(encryption_names)) inner_encrypted_with = self._encrypt_message_with_all(encryption_names) if self.DEBUGGING: self._log_message('message after encryption:\n{}'.format( self.crypto_message.get_email_message().to_string())) except MessageException as message_exception: raise MessageException(value=message_exception.value) except Exception as exception: self._log_error(str(exception)) except IOError as io_error: self._log_error(io_error.value) return inner_encrypted_with
def _get_to_crypto_details(self, encryption_name, user_ids): ''' Get the recipient details needed to encrypt a message. ''' from_user = self.crypto_message.smtp_sender() to_user = self.crypto_message.smtp_recipient() to_user_id = utils.get_user_id_matching_email(to_user, user_ids) if to_user_id is None: ready_to_encrypt = False self._log_message("No {} key for {}".format(encryption_name, to_user)) else: try: contacts.is_key_ok(to_user_id, encryption_name) ready_to_encrypt = True except CryptoException as exception: to_user_id = None ready_to_encrypt = False self._log_exception(exception.value) self._log_message('raising message exception in _get_to_crypto_details') raise MessageException(value=exception.value) return to_user_id, ready_to_encrypt
def _check_content(self, part): ''' Make sure we can read the content. >>> from goodcrypto_tests.mail.message_utils import get_basic_email_message >>> good_message = get_basic_email_message() >>> validator = Validator(good_message) >>> validator._check_content(validator.email_message) ''' content_type = part.get_header('Content-Type') self.log_message("MIME part content type: {}".format(content_type)) content = part.get_content() if isinstance(content, MIMEMultipart): count = 0 parts = content for sub_part in parts: self._check_content(sub_part) count += 1 self.log_message('parts in message: {}'.format(count)) if count != parts.getCount(): self.why = "Unable to read all content. Reported: '{}', read: {}".format( parts.getCount(), count) raise MessageException(self.why)
def encrypt_text_message(crypto_message, crypto, users_dict): ''' Encrypt a plain text message. ''' def encrypt_text_part(content, charset, crypto, users_dict): if DEBUGGING: log_message('type of content: {}'.format(type(content))) log_message('charset: {}'.format(charset)) result_ok = True try: data = bytearray(content, charset) except UnicodeEncodeError as uee: error_message = str(uee) result_ok = False if result_ok: ciphertext, error_message = encrypt_byte_array( data, crypto, users_dict) # if we encrypted successfully, save the results if ciphertext is not None and len(ciphertext) > 0: crypto_message.get_email_message().get_message().set_payload( ciphertext) from_user = users_dict[FROM_KEYWORD] log_message('from user: {}'.format(from_user)) log_message('passcode: {}'.format( users_dict[PASSCODE_KEYWORD])) set_sigs(crypto_message, from_user, users_dict[PASSCODE_KEYWORD]) result_ok = True else: result_ok = False return result_ok, error_message log_message('encrypting a text message') error_message = None email_message = crypto_message.get_email_message() if is_multipart_message(email_message): for part in email_message.walk(): charset, __ = get_charset(part) log_message( 'char set while encrypting text part: {}'.format(charset)) result_ok, error_message = encrypt_text_part( part.get_payload(), charset, crypto, users_dict) if not result_ok: break else: final_content = email_message.get_content() if DEBUGGING: log_message(' content:\n{!s}'.format(final_content)) charset, __ = get_charset(final_content) log_message( 'char set while encrypting text message: {}'.format(charset)) result_ok, error_message = encrypt_text_part(final_content, charset, crypto, users_dict) # if we encrypted successfully, save the results if result_ok: crypto_message.set_filtered(True) crypto_message.set_crypted(True) elif error_message is not None: raise MessageException(value=error_message)
def encrypt_message(crypto_message, data): encryption_ready = False encrypted_with = [] # use the metadata address' encryption to_metadata_address = metadata.get_metadata_address(email=to_user) encryption_names = contacts.get_encryption_names(to_metadata_address) log_message('{} encryption software for: {}'.format( encryption_names, to_metadata_address)) if len(encryption_names) < 1: error_message = i18n( 'Unable to protect metadata because there are no encryption programs for {}.' .format(to_metadata_address)) log_message(error_message) raise MessageException(value=error_message) else: # encrypt with each common encryption program for encryption_name in encryption_names: ready, to_metadata_address, __ = metadata.get_metadata_user_details( to_user, encryption_name) log_message('to metadata ready {} '.format(ready)) if ready: ready, from_metadata_address, passcode = metadata.get_from_metadata_user_details( from_user, encryption_name) log_message('metadata keys ready {}'.format(ready)) if ready: log_message( 'protecting metadata with {}'.format(encryption_names)) # if we're ready with any key, then the encryption is ready encryption_ready = True from_user_id = get_email(from_metadata_address) to_user_id = get_email(to_metadata_address) crypto_message.set_smtp_sender(from_user_id) crypto_message.set_smtp_recipient(to_user_id) # use the default charset to prevent metadata leakage charset, __ = get_charset(constants.DEFAULT_CHAR_SET) users_dict = { TO_KEYWORD: to_user_id, FROM_KEYWORD: from_user_id, PASSCODE_KEYWORD: passcode, CHARSET_KEYWORD: charset } crypto = CryptoFactory.get_crypto( encryption_name, get_classname(encryption_name)) ciphertext, error_message = encrypt_byte_array( data, crypto, users_dict) if ciphertext is not None and len(ciphertext) > 0: crypto_message.get_email_message().get_message( ).set_payload(ciphertext) crypto_message.add_public_key_to_header( users_dict[FROM_KEYWORD]) set_sigs(crypto_message, from_user_id, passcode) crypto_message.set_filtered(True) crypto_message.set_crypted(True) # use the encrypted data for the next level of encryption data = ciphertext encrypted_with.append(encryption_name) else: log_message( 'unable to encrypt the metadata with {}'.format( encryption_name)) raise MessageException(value=error_message) else: log_message('unable to protect metadata with {}'.format( encryption_name)) return encryption_ready, encrypted_with
def create_protected_message(from_user, to_user, data, message_id): ''' Create a new message that protects the metadata. ''' def start_crypto_message(): from goodcrypto.mail.message.crypto_message import CryptoMessage # start a new crypto message from_metadata_user = metadata.get_metadata_address(email=from_user) to_metadata_user = metadata.get_metadata_address(email=to_user) crypto_message = CryptoMessage() crypto_message.get_email_message().add_header( mime_constants.FROM_KEYWORD, from_metadata_user) crypto_message.get_email_message().add_header( mime_constants.TO_KEYWORD, to_metadata_user) crypto_message.get_email_message().add_header( mime_constants.MESSAGE_ID_KEYWORD, message_id) # include the timestamp because some MTAs/spam filters object if it's not set crypto_message.get_email_message().add_header( mime_constants.DATE_KEYWORD, datetime.utcnow().isoformat(str(' '))) return crypto_message def encrypt_message(crypto_message, data): encryption_ready = False encrypted_with = [] # use the metadata address' encryption to_metadata_address = metadata.get_metadata_address(email=to_user) encryption_names = contacts.get_encryption_names(to_metadata_address) log_message('{} encryption software for: {}'.format( encryption_names, to_metadata_address)) if len(encryption_names) < 1: error_message = i18n( 'Unable to protect metadata because there are no encryption programs for {}.' .format(to_metadata_address)) log_message(error_message) raise MessageException(value=error_message) else: # encrypt with each common encryption program for encryption_name in encryption_names: ready, to_metadata_address, __ = metadata.get_metadata_user_details( to_user, encryption_name) log_message('to metadata ready {} '.format(ready)) if ready: ready, from_metadata_address, passcode = metadata.get_from_metadata_user_details( from_user, encryption_name) log_message('metadata keys ready {}'.format(ready)) if ready: log_message( 'protecting metadata with {}'.format(encryption_names)) # if we're ready with any key, then the encryption is ready encryption_ready = True from_user_id = get_email(from_metadata_address) to_user_id = get_email(to_metadata_address) crypto_message.set_smtp_sender(from_user_id) crypto_message.set_smtp_recipient(to_user_id) # use the default charset to prevent metadata leakage charset, __ = get_charset(constants.DEFAULT_CHAR_SET) users_dict = { TO_KEYWORD: to_user_id, FROM_KEYWORD: from_user_id, PASSCODE_KEYWORD: passcode, CHARSET_KEYWORD: charset } crypto = CryptoFactory.get_crypto( encryption_name, get_classname(encryption_name)) ciphertext, error_message = encrypt_byte_array( data, crypto, users_dict) if ciphertext is not None and len(ciphertext) > 0: crypto_message.get_email_message().get_message( ).set_payload(ciphertext) crypto_message.add_public_key_to_header( users_dict[FROM_KEYWORD]) set_sigs(crypto_message, from_user_id, passcode) crypto_message.set_filtered(True) crypto_message.set_crypted(True) # use the encrypted data for the next level of encryption data = ciphertext encrypted_with.append(encryption_name) else: log_message( 'unable to encrypt the metadata with {}'.format( encryption_name)) raise MessageException(value=error_message) else: log_message('unable to protect metadata with {}'.format( encryption_name)) return encryption_ready, encrypted_with try: log_message('creating a new message that protects the metadata') crypto_message = start_crypto_message() if data is None: log_message('no data to encrypt') else: ready, encrypted_with = encrypt_message(crypto_message, data) if crypto_message.is_crypted(): crypto_message.set_metadata_crypted(True) crypto_message.set_metadata_crypted_with(encrypted_with) log_message( 'metadata encrypted with: {}'.format(encrypted_with)) if DEBUGGING: log_message('metadata message:\n{}'.format( crypto_message.get_email_message().to_string())) elif not ready: error_message = i18n( 'Unable to protect metadata because a key is missing.') log_message(error_message) raise MessageException(value=error_message) else: error_message = i18n( 'Unable to protect metadata even though keys for both servers exist.' ) log_message(error_message) raise MessageException(value=error_message) except MessageException as message_exception: log_message('raising MessageException') raise MessageException(value=message_exception.value) except: error_message = i18n( 'Unable to protect metadata due to an unexpected error.') log_message(error_message) log_message('EXCEPTION - see syr.exception.log for details') record_exception() raise MessageException(value=error_message) return crypto_message
def process_message(self): ''' Process a message and encrypt if possible, bounce if unable to encrypt and encryption required, or add a warning about the danger of sending unencrypted messages. Also, add clear sig and DKIM sig if user opted for either or both. ''' filtered = encrypted = False inner_encrypted_with = [] # a place for the original message in case metadata is protected original_crypto_message = None try: if self.crypto_message is None: self._log_message('no crypto message defined') self.crypto_message = CryptoMessage() from_user = to_user = None else: from_user = self.crypto_message.smtp_sender() to_user = self.crypto_message.smtp_recipient() self._log_message("trying to encrypt message from {} to {}".format(from_user, to_user)) self.verification_code = gen_verification_code() self.ready_to_protect_metadata = is_ready_to_protect_metadata(from_user, to_user) contact = contacts.get(to_user) if contact is None: never_encrypt = True encryption_names = [] if options.use_keyservers(): # if there's no contact, then start another job # that can determine whether we can find a key self._start_check_for_encryption(from_user, to_user) else: never_encrypt = contact.outbound_encrypt_policy == NEVER_ENCRYPT_OUTBOUND encryption_names = contacts.get_encryption_names(to_user) if len(encryption_names) <= 0 or never_encrypt: self._process_plain_message(from_user, to_user, never_encrypt, encryption_names) else: inner_encrypted_with = self._process_encrypt_message(encryption_names) if self.ready_to_protect_metadata: original_crypto_message = copy.copy(self.crypto_message) self._protect_metadata(from_user, to_user, inner_encrypted_with) else: self._process_no_metadata_message(to_user) if self.crypto_message.is_processed(): self._log_message('message processed and awaiting bundling') else: self._finish_processing_message(inner_encrypted_with, original_crypto_message) except MessageException as message_exception: raise MessageException(value=message_exception.value) except Exception as IOError: record_exception() self._log_message('EXCEPTION - see syr.exception.log for details') if self.crypto_message is not None: self._log_message(' final status: filtered: {} encrypted: {}'.format( self.crypto_message.is_filtered(), self.crypto_message.is_crypted())) return self.crypto_message
def keys_in_header(self, crypto_message): ''' Return true if there are public keys in the message's header. ''' header_contains_key_info = False try: good_key = True from_user = crypto_message.smtp_sender() accepted_crypto_packages = crypto_message.get_accepted_crypto_software( ) if accepted_crypto_packages is not None and len( accepted_crypto_packages) > 0: self.log_message( "checking for {} keys".format(accepted_crypto_packages)) for encryption_name in accepted_crypto_packages: # see if there's a the key block for this encryption program header_name = get_public_key_header_name(encryption_name) key_block = get_multientry_header( crypto_message.get_email_message().get_message(), header_name) # see if there's a plain key block if ((key_block is None or len(key_block) <= 0) and len(accepted_crypto_packages) == 1): self.log_message( "no {} public key in header so trying generic header" .format(encryption_name)) key_block = get_multientry_header( crypto_message.get_email_message().get_message(), PUBLIC_KEY_HEADER) if key_block is not None and len(key_block) > 0: header_contains_key_info = True user_ids = [] key_crypto = KeyFactory.get_crypto( encryption_name, crypto_software.get_key_classname(encryption_name)) if key_crypto is None: id_fingerprint_pairs = None self.log_message( 'no key crypto for {}'.format(encryption_name)) else: id_fingerprint_pairs = key_crypto.get_id_fingerprint_pairs( key_block) for (user_id, __) in id_fingerprint_pairs: user_ids.append(user_id) self.log_message( "key block includes key for {}".format( user_ids)) # don't consider a key valid if it's not from the sender if from_user not in user_ids: tag = notices.report_bad_header_key( self.recipient_to_notify, from_user, user_ids, encryption_name, crypto_message) self.log_message('tag: {}'.format(tag)) if crypto_message.get_email_message( ).is_probably_pgp(): crypto_message.drop(dropped=True) self.log_message( 'serious error in header so original message sent as attchment' ) raise MessageException(value=i18n(tag)) self.update_accepted_crypto(from_user, accepted_crypto_packages) except MessageException as message_exception: raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') if crypto_message is not None: crypto_message.add_error_tag_once(self.UNEXPECTED_ERROR) return header_contains_key_info
def process_message(self): ''' If the message is encrypted, try to decrypt it. If it's not encrypted, add a warning about the dangers of unencrypted messages. See unittests for usage as the test set up is too complex for a doctest. ''' try: filtered = decrypted = False from_user = self.crypto_message.smtp_sender() to_user = self.crypto_message.smtp_recipient() if options.verify_dkim_sig(): self.crypto_message, dkim_sig_verified = decrypt_utils.verify_dkim_sig( self.crypto_message) if options.dkim_delivery_policy() == DKIM_DROP_POLICY: self.log_message('verified dkim signature ok: {}'.format( dkim_sig_verified)) elif dkim_sig_verified: self.log_message('verified dkim signature') else: self.log_message( 'unable to verify dkim signature, but dkim policy is to just warn' ) self.log_message( "checking if message from {} to {} needs decryption".format( from_user, to_user)) if Decrypt.DEBUGGING: self.log_message( 'logged original message headers in goodcrypto.message.utils.log' ) utils.log_message_headers(self.crypto_message, tag='original message headers') header_keys = HeaderKeys() if options.auto_exchange_keys(): header_contains_key_info = header_keys.manage_keys_in_header( self.crypto_message) else: header_contains_key_info = header_keys.keys_in_header( self.crypto_message) if self.crypto_message.is_dropped(): decrypted = False self.log_message( "message dropped because of bad key in header") self.log_message( 'logged dropped message headers in goodcrypto.message.utils.log' ) utils.log_message_headers(self.crypto_message, tag='dropped message headers') else: message_char_set, __ = get_charset( self.crypto_message.get_email_message()) self.log_message( 'message char set: {}'.format(message_char_set)) if self.crypto_message.get_email_message().is_probably_pgp(): decrypted, decrypted_with = self.decrypt_message() if not decrypted and self.crypto_message.is_metadata_crypted( ): tags.add_metadata_tag(self.crypto_message) self.log_message( "message only encrypted with metadata key") else: decrypt_utils.verify_clear_signed(from_user, self.crypto_message) if self.crypto_message.is_metadata_crypted(): tags.add_metadata_tag(self.crypto_message) self.log_message( "message only encrypted with metadata key") else: tags.add_unencrypted_warning(self.crypto_message) filtered = True self.log_message( "message doesn't appear to be encrypted at all") # create a private key for the recipient if there isn't one already add_private_key(to_user) self.need_to_send_metadata_key = ( # if the metadata wasn't encrypted not self.crypto_message.is_metadata_crypted() and # but the sender's key was in the header so we know the sender uses GoodCrypto private server header_contains_key_info and # and we don't have the sender's metadata key len( contacts.get_encryption_names( get_metadata_address(email=from_user))) < 1) self.log_message('need to send metadata key: {}'.format( self.need_to_send_metadata_key)) self.log_message('message content decrypted: {}'.format(decrypted)) # finally save a record so the user can verify the message was received securely if (decrypted or self.crypto_message.is_metadata_crypted() or self.crypto_message.is_private_signed() or self.crypto_message.is_clear_signed()): decrypt_utils.add_history_and_verification(self.crypto_message) if decrypted or self.crypto_message.is_metadata_crypted(): self.crypto_message.set_crypted(True) if not decrypted: self.log_message('metadata tag: {}'.format( self.crypto_message.get_tag())) analyzer = OpenPGPAnalyzer() if analyzer.is_encrypted( self.crypto_message.get_email_message().get_content()): tags.add_extra_layer_warning(self.crypto_message) else: self.crypto_message.set_crypted(False) decrypted_tags_filtered = tags.add_decrypted_tags_to_message( self.crypto_message) if decrypted_tags_filtered: filtered = True if filtered and not self.crypto_message.is_filtered(): self.crypto_message.set_filtered(True) self.log_message( "finished adding tags to decrypted message; filtered: {}". format(self.crypto_message.is_filtered())) self.log_message("message originally encrypted with: {}".format( self.crypto_message.get_crypted_with())) self.log_message("metadata originally encrypted with: {}".format( self.crypto_message.get_metadata_crypted_with())) decrypt_utils.re_mime_encode(self.crypto_message) if self.DEBUGGING: self.log_message( 'logged final decrypted headers in goodcrypto.message.utils.log' ) utils.log_message_headers(self.crypto_message, tag='final decrypted headers') self.log_message( ' final status: filtered: {} decrypted: {}'.format( self.crypto_message.is_filtered(), self.crypto_message.is_crypted())) except MessageException as message_exception: self.log_message(message_exception) raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') return self.crypto_message
def _manage_public_key(self, from_user, crypto_message, key_crypto, key_block, id_fingerprint_pairs): ''' Manage a public key for the encryption software (internal use only). ''' tag = None drop = False encryption_name = key_crypto.get_name() user_ids = [] for (user_id, __) in id_fingerprint_pairs: user_ids.append(user_id) self.log_message("key block includes key for {}".format(user_ids)) if from_user in user_ids: # see if we already have a key for this ID or if it has expired saved_fingerprint, verified, __ = contacts.get_fingerprint( from_user, encryption_name) crypto_fingerprint, expiration = key_crypto.get_fingerprint( from_user) self.log_message("{} {} saved fingerprint {}".format( from_user, encryption_name, saved_fingerprint)) self.log_message("{} {} crypto fingerprint {} expires {}".format( from_user, encryption_name, crypto_fingerprint, expiration)) self.log_message("{} {} id fingerprint pairs {}".format( from_user, encryption_name, id_fingerprint_pairs)) if crypto_fingerprint is None: if saved_fingerprint is None: self.log_message("importing new key") tag = self._import_new_key(from_user, encryption_name, key_block, id_fingerprint_pairs) else: self.log_message("checking if key matches") drop = crypto_message.get_email_message().is_probably_pgp() key_matches, key_error = self._key_matches( encryption_name, saved_fingerprint, id_fingerprint_pairs) self.log_message("key matches: {} / key error: {}".format( key_matches, key_error)) if key_error: tag = notices.report_error_verifying_key( self.recipient_to_notify, from_user, encryption_name, crypto_message) else: tag = notices.report_missing_key( self.recipient_to_notify, from_user, key_matches, id_fingerprint_pairs, crypto_message) else: if saved_fingerprint is None: # remember the fingerprint for the future saved_fingerprint = crypto_fingerprint if (strip_fingerprint(crypto_fingerprint).lower() == strip_fingerprint(saved_fingerprint).lower()): key_matches, key_error = self._key_matches( encryption_name, crypto_fingerprint, id_fingerprint_pairs) if key_error: tag = notices.report_error_verifying_key( self.recipient_to_notify, from_user, encryption_name, crypto_message) elif key_matches: if key_crypto.fingerprint_expired(expiration): drop = crypto_message.get_email_message( ).is_probably_pgp() tag = notices.report_expired_key( self.recipient_to_notify, from_user, encryption_name, expiration, crypto_message) else: self.log_message(' same fingerprint: {}'.format( saved_fingerprint)) # warn user if unable to save the fingerprint, but proceed if not self.update_fingerprint( from_user, encryption_name, crypto_fingerprint): tag = notices.report_db_error( self.recipient_to_notify, from_user, encryption_name, crypto_message) else: if self.DEBUGGING: self.log_message('{} key block\n{}'.format( from_user, key_crypto.export_public(from_user))) drop = crypto_message.get_email_message( ).is_probably_pgp() tag = notices.report_replacement_key( self.recipient_to_notify, from_user, encryption_name, id_fingerprint_pairs, crypto_message) else: drop = crypto_message.get_email_message().is_probably_pgp() tag = notices.report_mismatched_keys( self.recipient_to_notify, from_user, encryption_name, crypto_message) else: if len(user_ids) > 0: drop = crypto_message.get_email_message().is_probably_pgp() tag = notices.report_bad_header_key(self.recipient_to_notify, from_user, user_ids, encryption_name, crypto_message) else: self.log_message( 'no importable keys found\n{}'.format(key_block)) self.log_message('tag: {}'.format(tag)) if drop: crypto_message.drop(dropped=True) self.log_message( 'serious error in header so original message sent as attchment' ) raise MessageException(value=i18n(tag)) return tag
def manage_keys_in_header(self, crypto_message): ''' Manage all the public keys in the message's header. ''' header_contains_key_info = False try: from_user = crypto_message.smtp_sender() self.recipient_to_notify = crypto_message.smtp_recipient() # all notices about a metadata address goes to the admin if is_metadata_address(self.recipient_to_notify): self.recipient_to_notify = get_admin_email() name, address = parse_address(from_user) if address is None or crypto_message is None or crypto_message.get_email_message( ) is None: self.log_message('missing data so cannot import key') self.log_message(' from user: {}'.format(from_user)) self.log_message(' address: {}'.format(address)) self.log_message( ' crypto message: {}'.format(crypto_message)) if crypto_message is not None: self.log_message(' email message: {}'.format( crypto_message.get_email_message())) else: accepted_crypto_packages = crypto_message.get_accepted_crypto_software( ) if accepted_crypto_packages is None or len( accepted_crypto_packages) <= 0: self.log_message( "checking for default key for {} <{}>".format( name, address)) tag = self._manage_key_header( address, crypto_message, KeyFactory.get_default_encryption_name(), PUBLIC_KEY_HEADER) else: self.log_message("checking for {} keys".format( accepted_crypto_packages)) for encryption_name in accepted_crypto_packages: # see if there's a the key block for this encryption program header_name = get_public_key_header_name( encryption_name) key_block = get_multientry_header( crypto_message.get_email_message().get_message(), header_name) # see if there's a plain key block if ((key_block is None or len(key_block) <= 0) and len(accepted_crypto_packages) == 1): self.log_message( "no {} public key in header so trying generic header" .format(encryption_name)) key_block = get_multientry_header( crypto_message.get_email_message().get_message( ), PUBLIC_KEY_HEADER) tag = self._manage_key_header(address, crypto_message, encryption_name, key_block) header_contains_key_info = True self.update_accepted_crypto(from_user, accepted_crypto_packages) except MessageException as message_exception: self.log_message(message_exception.value) raise MessageException(value=message_exception.value) except: record_exception() self.log_message('EXCEPTION - see syr.exception.log for details') if crypto_message is not None: crypto_message.add_error_tag_once(self.UNEXPECTED_ERROR) self.log_message( 'header_contains_key_info: {}'.format(header_contains_key_info)) return header_contains_key_info
def __init__(self, message_or_file=None): ''' Creates an EmailMessage from a Message or a file. Non-mime messages are converted to MIME "text/plain". >>> email_message = EmailMessage() >>> type(email_message) <class 'goodcrypto.mail.message.email_message.EmailMessage'> ''' self.bad_header_lines = [] self.parser = Parser() self._last_charset = constants.DEFAULT_CHAR_SET self._log = self._message = None if message_or_file is None: self._message = Message() elif isinstance(message_or_file, Message): self._message = message_or_file elif isinstance(message_or_file, EmailMessage): self._message = message_or_file.get_message() else: try: if isinstance(message_or_file, IOBase) or isinstance(message_or_file, StringIO): self.log_message('about to parse a message from a file') try: self._message = self.parser.parse(message_or_file) self.log_message('parsed message from file') except TypeError: message_or_file.seek(0, os.SEEK_SET) self.parser = BytesParser() self._message = self.parser.parse(message_or_file) self.log_message('parsed message from file as bytes') else: try: self.log_message('about to parse a message from a string') self._message = self.parser.parsestr(message_or_file) self.log_message('parsed message from string') except TypeError: self.parser = BytesParser() self._message = self.parser.parsebytes(message_or_file) self.log_message('parsed message from bytes') if not self.validate_message(): self._create_good_message_from_bad(message_or_file) except Exception: try: self.log_message('EXCEPTION - see syr.exception.log for details') record_exception() self._create_good_message_from_bad(message_or_file) # if we still don't have a good message, then blow up if not self.validate_message(): self.log_message('unable to create a valid message') raise MessageException() except Exception: record_exception() if self.DEBUGGING: try: self.log_message(self.to_string()) except: pass