This is a revised copy of the Mail Crypt library based on the recommendations from the last post, as well as reviewing recommendations from the first post that I misunderstood.
[Last post] Mail Crypt Library for encrypted email
[First Post] Mail Crypt CLI encrypted email wrapper for Gmail
Change Log:
- Removed try/except block from tag/sig status so end user gets the error.
- Un-bundled context manager so public and private keys are handled separately.
- encrypted message return uses join and hex with for loop rather that .hex after every item.
- decrypt message now unpack each item into there own var rather than as a list.
- import keys now unpacks into 2 var rather than list.
- get_emails unpack just what is need into data and rest is pass to underscore.
- created fetch function in email to replace 3 identical fetch calls in Email. (get_emails, read_encrypted, read)
- Keys.export_keys renamed to Keys.save_keys to have symmetric nomenclature.
- Split Keys class into CreateKeys, PublicKeysStore, and PersonalKeys.
- rsa_key_length is no longer a class constant but now an arg, change_rsa_key_length class method removed.
- split load my keys into get_public and get_private.
- removed the private and public key args from MailCrypt so keys must now be passed into encrypt and decrypt function.
- static method decorator added to decrypt message.
- update keys method removed from MailCrypt since MailCrypt no longer handles keys.
- removed constant of aes_session_key_length from MailCrypt, its now an arg passed into encrypt_msg with a defalut of 32.
- encrypt message is now a static method.
- Email class split into EmailSMTP and EmailIMAP (Grouped based on which server is used).
- SMTP port is no longer a constant, can be passed in or default used.
- removed change smtp port method since port is now an arg.
- fetch now replaces read in EmailIMAP.
- PublicKey object now must be passed into EmailSMTP to handle public key look up.
"""Library for send and receiveing encrypted emails."""
import pickle
import email
import imaplib
import smtplib
from Crypto.Hash import SHA512
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Signature import pss
class MailCrypt:
"""Core compents for encryption/decryption and key generation."""
@staticmethod
def encrypt_msg(message, recpient_public_key, private_key, aes_session_key_length=32):
"""Generates a session key to use with AES to encrypt the message, then
encrypts the session key with the recipients public key. Everything
is returned in hex format to be better sent over email."""
session_key = get_random_bytes(aes_session_key_length)
aes_cipher = AES.new(session_key, AES.MODE_EAX)
aes_cipher_text, tag = aes_cipher.encrypt_and_digest(message)
pub = PKCS1_OAEP.new(recpient_public_key).encrypt(session_key)
priv = pss.new(private_key).sign(SHA512.new(message))
return ' '.join(
part.hex() for part in (
aes_cipher_text,
tag,
aes_cipher.nonce,
pub,
priv,
)
)
@staticmethod
def decrypt_msg(message, sender_public_key, private_key):
"""Splits the message into its sections
Decrypts the session key, then decrypts the message body.
If aes_cipher.verify throws an error that means an invalid
tag was provided
If pss.new throws an error that means the message signature
is invalid"""
aes_cipher_text, tag, nonce, enc_session_key, signature = (
value.encode().fromhex(value)
for value in message.split(' '))
aes_cipher = AES.new(
PKCS1_OAEP.new(private_key).decrypt(enc_session_key),
AES.MODE_EAX,
nonce=nonce,
)
clear_text = aes_cipher.decrypt(aes_cipher_text)
aes_cipher.verify(tag)
pss.new(sender_public_key).verify(SHA512.new(clear_text), signature)
return clear_text
class PublicKey:
"""Handles public key storage and retrieval."""
def __init__(self):
self.key_dict = {}
def load_keys(self):
"""Read public keys in from file. Must be in same folder
as script is run from."""
with open('public_key_bank.pkl', 'rb') as fpublic_key_file:
self.key_dict = pickle.load(fpublic_key_file)
def save_keys(self):
"""Saves key_dict to file."""
with open('public_key_bank.pkl', 'wb') as fpublic_key_file:
pickle.dump(self.key_dict, fpublic_key_file)
def add_key(self, address, key):
"""Adds key to to key_dict."""
self.key_dict[address] = key
def retreive_key(self, address):
"""Retrieves public key based on email."""
return RSA.import_key(self.key_dict[address])
def remove_key(self, address):
"""Removes key from dict"""
self.key_dict.pop(address)
class PersonalKeys:
"""Handles users public and private keys."""
def __init__(self):
self.my_private_key = None
self.my_public_key = None
def get_private(self, passwd):
"""Loads private key in from file."""
with open('private_key.pem', 'r') as fprivate_key_save:
self.my_private_key = RSA.import_key(fprivate_key_save.read(), passphrase=passwd)
def get_public(self):
"""Loads public key in from file."""
with open('my_public_key.pem', 'r') as fpublic_key_save:
self.my_public_key = RSA.import_key(fpublic_key_save.read())
class CreateKeys:
"""Handles key pair creation and storage."""
def __init__(self, rsa_key_length=4096):
self.rsa_key_length = rsa_key_length
def generate_keys(self, passwd):
"""Generates public and private key pairs and exports them as .pem files."""
private_key = RSA.generate(self.rsa_key_length)
public_key = private_key.publickey()
with open('my_public_key.pem', 'wb') as fpub:
fpub.write(public_key.export_key('PEM'))
with open('private_key.pem', 'wb') as fpri:
fpri.write(private_key.export_key('PEM', passphrase=passwd))
def generate_keys_test(self):
""""Used for testing, returns key pair."""
private_key = RSA.generate(self.rsa_key_length)
public_key = private_key.publickey()
return private_key, public_key
class EmailSMTP:
"""Handles the SMTP functionality."""
def __init__(self, username, passwd, server_address, pub_key_obj, per_key_obj, port=465):
self.smtp = smtplib.SMTP_SSL(server_address, port)
self.smtp.ehlo()
self.username = username
self.smtp.login(username, passwd)
self.publickeys = pub_key_obj
self.private = per_key_obj
def close_connection(self):
"""Closes connection to server."""
self.smtp.close()
def send(self, recipient, message):
"""Sends plain text email."""
self.smtp.sendmail(self.username, recipient, message)
def send_encrypted(self, recipient, message):
"""Sends encrypted message."""
message = MailCrypt.encrypt_msg(message.encode(),
self.publickeys.retreive_key(recipient),
self.private.my_private_key)
self.smtp.sendmail(self.username, recipient, message)
def share_public_key(self, recipient):
"""Sends public key."""
self.send(recipient, self.private.my_public_key.export_key('PEM'))
class EmailIMAP:
"""Handles the IMAP sever funcionality"""
def __init__(self, username, passwd, server_address):
self.imap = imaplib.IMAP4_SSL(server_address)
self.imap.login(username, passwd)
self.imap.select('inbox')
def close_connection(self):
"""Logs out and closes connection to the server"""
self.imap.logout()
self.imap.close()
def get_emails(self):
"""Yeilds uid and senders address for every message
in the inbox folder."""
_, data, *_ = self.imap.uid('search', None, 'ALL')
for uid in data[0].decode().split(' '):
sender, _ = self.fetch(uid)
yield uid, sender
def fetch(self, uid):
"""returns sender address and message payload"""
_, email_data = self.imap.uid('fetch', uid, '(RFC822)')
msg = email.message_from_bytes(email_data[0][1])
return msg['From'], msg.get_payload()
def read_encrypted(self, uid, sender_public, private):
"""Fetches email from given uid and returns clear text."""
_, payload = self.fetch(uid)
return MailCrypt.decrypt_msg(payload, sender_public, private)
def mark_delete(self, uid):
"""Moves the specified email to trash folder. If useing email
provider other than gmail 'Trash' needs to be changed to
whatever folder that service uses."""
self.imap.uid('store', uid, '+X-GM-LABELS', '(\\Trash)')
def delete_all(self):
"""Empties the trash folder."""
self.imap.expunge()
def import_key(self):
"""Checks message payloads for public keys, if found it
yeilds the senders email address and the public key."""
for uid, sender in self.get_emails():
sender, msg_body = self.fetch(uid)
if 'PUBLIC' in msg_body:
yield sender, msg_body