Code sourced from these projects:
- * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
+ * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/edward.tgz
* https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
* http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
"""
'sigkey_missing' is set to True if edward doesn't have the key it needs to
verify the signature on a block of text.
+ 'key_cannot_encrypt' is set to True if pubkeys or sigs' keys in the payload
+ piece are not capable of encryption, are revoked or expired, for instance.
+
'keys' is a list of fingerprints of keys obtained in public key blocks.
"""
plainobj = None
sigs = []
sigkey_missing = False
+ key_cannot_encrypt = False
keys = []
'sig_success' is set to True if edward could to some extent verify the
signature of a signed part of the message to edward.
+ 'key_can_encrypt' is set to True if a key which can be encrypted to has
+ been found.
+
'sig_failure' is set to True if edward could not verify a siganture.
'pubkey_success' is set to True if edward successfully imported a public
'sigkey_missing' is set to True if edward doesn't have the public key
needed for signature verification.
+ 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload
+ piece of the message are not capable of encryption.
+
'have_repy_key' is set to True if edward has a public key to encrypt its
reply to.
"""
decrypt_success = False
sig_success = False
pubkey_success = False
+ key_can_encrypt = False
+ decrypt_failure = False
sig_failure = False
sigkey_missing = False
+ key_cannot_encrypt = False
+
have_reply_key = False
gpgme_ctx = get_gpg_context(edward_config.gnupghome,
edward_config.sign_with_key)
- email_text = sys.stdin.read()
- email_struct = parse_pgp_mime(email_text, gpgme_ctx)
+ email_bytes = sys.stdin.buffer.read()
+ email_struct = parse_pgp_mime(email_bytes, gpgme_ctx)
- email_to, email_from, email_subject = email_to_from_subject(email_text)
+ email_to, email_from, email_subject = email_to_from_subject(email_bytes)
lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname)
replyinfo_obj = ReplyInfo()
return gpgme_ctx
-def parse_pgp_mime (email_text, gpgme_ctx):
+def parse_pgp_mime (email_bytes, gpgme_ctx):
"""Parses the email for mime payloads and decrypts/verfies signatures.
This function creates a representation of a mime or plaintext email with
does some very basic signature verification on those parts.
Args:
- email_text: an email message in string format
+ email_bytes: an email message in byte string format
gpgme_ctx: a gpgme context
Returns:
imported payloads
"""
- email_struct = email.parser.Parser().parsestr(email_text)
+ email_struct = email.parser.BytesParser().parsebytes(email_bytes)
eddymsg_obj = parse_mime(email_struct)
split_payloads(eddymsg_obj)
an instance of EddyMsg, potentially a recursive one.
"""
- eddymsg_obj = EddyMsg()
+ eddymsg_obj = get_subpart_data(msg_struct)
if msg_struct.is_multipart() == True:
payloads = msg_struct.get_payload()
eddymsg_obj.multipart = True
eddymsg_obj.subparts = list(map(parse_mime, payloads))
- else:
- eddymsg_obj = get_subpart_data(msg_struct)
-
return eddymsg_obj
eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
Post:
- Decryption, verification and key imports occur. the gpg_data member of
- PayloadPiece objects get filled in with GPGData objects.
+ Decryption, verification and key imports occur. the gpg_data members of
+ PayloadPiece objects get filled in with GPGData objects with some of
+ their attributes set.
"""
if eddymsg_obj.multipart == True:
elif piece.piece_type == TxtType.message:
piece.gpg_data = GPGData()
- (plaintext, sigs, sigkey_missing) = decrypt_block(piece.string, gpgme_ctx)
+ (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = decrypt_block(piece.string, gpgme_ctx)
piece.gpg_data.sigkey_missing = sigkey_missing
+ piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
- if plaintext:
+ if plaintext_b:
piece.gpg_data.decrypted = True
piece.gpg_data.sigs = sigs
# recurse!
- piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+ piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
continue
# if not encrypted, check to see if this is an armored signature.
- (plaintext, sigs, sigkey_missing) = verify_sig_message(piece.string, gpgme_ctx)
+ (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx)
piece.gpg_data.sigkey_missing = sigkey_missing
+ piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
- if plaintext:
+ if plaintext_b:
piece.piece_type = TxtType.signature
piece.gpg_data.sigs = sigs
# recurse!
- piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
+ piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
elif piece.piece_type == TxtType.pubkey:
- key_fps = add_gpg_key(piece.string, gpgme_ctx)
+ piece.gpg_data = GPGData()
+
+ (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx)
+
+ piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
if key_fps != []:
- piece.gpg_data = GPGData()
piece.gpg_data.keys = key_fps
elif piece.piece_type == TxtType.detachedsig:
piece.gpg_data = GPGData()
for prev in prev_parts:
- (sig_fps, sigkey_missing) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
+ (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
piece.gpg_data.sigkey_missing = sigkey_missing
+ piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
if sig_fps != []:
piece.gpg_data.sigs = sig_fps
Post:
replyinfo_obj gets updated with decryption status, signing status, a
- potential signing key, and posession status of the public key for the
- signature.
+ potential signing key, posession status of the public key for the
+ signature and encryption capability status if that key is missing.
"""
- if piece.gpg_data == None or piece.gpg_data.plainobj == None:
+ if piece.gpg_data.plainobj == None:
+ replyinfo_obj.decrypt_failure = True
return
replyinfo_obj.decrypt_success = True
if replyinfo_obj.target_key != None:
return
- if piece.gpg_data.sigs != []:
- replyinfo_obj.target_key = piece.gpg_data.sigs[0]
- replyinfo_obj.sig_success = True
- get_signed_part = False
-
- else:
+ if piece.gpg_data.sigs == []:
if piece.gpg_data.sigkey_missing == True:
replyinfo_obj.sigkey_missing = True
+ if piece.gpg_data.key_cannot_encrypt == True:
+ replyinfo_obj.key_cannot_encrypt = True
+
# only include a signed message in the reply.
get_signed_part = True
+ else:
+ replyinfo_obj.target_key = piece.gpg_data.sigs[0]
+ replyinfo_obj.sig_success = True
+ get_signed_part = False
+
flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
# to catch public keys in encrypted blocks
replyinfo_obj has its fields updated.
"""
- if piece.gpg_data == None or piece.gpg_data.keys == []:
- pass
+ if piece.gpg_data.keys == []:
+ if piece.gpg_data.key_cannot_encrypt == True:
+ replyinfo_obj.key_cannot_encrypt = True
else:
replyinfo_obj.pubkey_success = True
replyinfo_obj has its fields updated.
"""
- if piece.gpg_data == None or piece.gpg_data.sigs == []:
+ if piece.gpg_data.sigs == []:
replyinfo_obj.sig_failure = True
if piece.gpg_data.sigkey_missing == True:
replyinfo_obj.sigkey_missing = True
+ if piece.gpg_data.key_cannot_encrypt == True:
+ replyinfo_obj.key_cannot_encrypt = True
+
else:
replyinfo_obj.sig_success = True
if replyinfo_obj.fallback_target_key == None:
replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
+ if (piece.piece_type == TxtType.signature):
+ # to catch public keys in signature blocks
+ prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
+
def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
"""For creating a string representation of a signed, encrypted part.
gpgme_ctx: the gpgme context
Return:
- The key object of the key of either the target_key or the fallback one
- if .target_key is not set. If the key cannot be loaded, then return
- None.
+ Nothing
Pre:
Loading a key requires that we have the public key imported. This
Post:
If the key can be loaded, then replyinfo_obj.reply_to_key points to the
public key object. If the key cannot be loaded, then the replyinfo_obj
- is marked as having no public key available.
+ is marked as having no public key available. If the key is not capable
+ of encryption, it will not be used, and replyinfo_obj will be marked
+ accordingly.
"""
for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key):
if key != None:
try:
encrypt_to_key = gpgme_ctx.get_key(key)
+
+ except gpgme.GpgmeError:
+ continue
+
+ if is_key_usable(encrypt_to_key):
replyinfo_obj.encrypt_to_key = encrypt_to_key
replyinfo_obj.have_reply_key = True
+ replyinfo_obj.key_can_encrypt = True
return
- except gpgme.GpgmeError:
- pass
+ else:
+ replyinfo_obj.key_cannot_encrypt = True
+
def write_reply (replyinfo_obj):
reply_plain = ""
+ if (replyinfo_obj.pubkey_success == True):
+ reply_plain += replyinfo_obj.replies['greeting']
+ reply_plain += "\n\n"
+
+
if replyinfo_obj.decrypt_success == True:
debug('decrypt success')
reply_plain += replyinfo_obj.replies['success_decrypt']
- reply_plain += "\n\n"
if (replyinfo_obj.sig_success == True) and (replyinfo_obj.have_reply_key == True):
debug('message quoted')
+ reply_plain += replyinfo_obj.replies['space']
+ reply_plain += replyinfo_obj.replies['quote_follows']
+ reply_plain += "\n\n"
quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
reply_plain += quoted_text
- reply_plain += "\n\n"
- else:
+ reply_plain += "\n\n"
+
+ elif replyinfo_obj.decrypt_failure == True:
debug('decrypt failure')
reply_plain += replyinfo_obj.replies['failed_decrypt']
reply_plain += "\n\n"
+
if replyinfo_obj.sig_success == True:
debug('signature success')
reply_plain += replyinfo_obj.replies['sig_success']
reply_plain += replyinfo_obj.replies['sig_failure']
reply_plain += "\n\n"
+
if (replyinfo_obj.pubkey_success == True):
debug('public key received')
reply_plain += replyinfo_obj.replies['public_key_received']
reply_plain += replyinfo_obj.replies['no_public_key']
reply_plain += "\n\n"
+ elif (replyinfo_obj.key_can_encrypt == False) \
+ and (replyinfo_obj.key_cannot_encrypt == True):
+ debug('bad public key')
+ reply_plain += replyinfo_obj.replies['no_public_key']
+ reply_plain += "\n\n"
+
+
+ if (reply_plain == ""):
+ debug('plaintext message')
+ reply_plain += replyinfo_obj.replies['failed_decrypt']
+ reply_plain += "\n\n"
+
reply_plain += replyinfo_obj.replies['signature']
reply_plain += "\n\n"
gpgme_ctx: the gpgme context
Returns:
- the fingerprint(s) of the imported key(s)
+ the fingerprint(s) of the imported key(s) which can be used for
+ encryption, and a boolean marking whether none of the keys are capable
+ of encryption.
"""
fp = io.BytesIO(key_block.encode('ascii'))
imports = []
key_fingerprints = []
+ key_cannot_encrypt = False
+
+ for import_res in imports:
+ fingerprint = import_res[0]
+
+ try:
+ key_obj = gpgme_ctx.get_key(fingerprint)
+ except:
+ pass
- if imports != []:
- for import_ in imports:
- fingerprint = import_[0]
+ if is_key_usable(key_obj):
key_fingerprints += [fingerprint]
+ key_cannot_encrypt = False
debug("added gpg key: " + fingerprint)
- return key_fingerprints
+ elif key_fingerprints == []:
+ key_cannot_encrypt = True
+
+ return (key_fingerprints, key_cannot_encrypt)
def verify_sig_message (msg_block, gpgme_ctx):
gpgme_ctx: the gpgme context
Returns:
- A tuple containing the plaintext of the signed part, the list of
- fingerprints of keys signing the data, and a boolean marking whether
- edward is missing all public keys for validating any of the signatures.
- If verification failed, perhaps because the message was also encrypted,
- then empty results are returned.
+ A tuple containing the plaintext bytes of the signed part, the list of
+ fingerprints of encryption-capable keys signing the data, a boolean
+ marking whether edward is missing all public keys for validating any of
+ the signatures, and a boolean marking whether all sigs' keys are
+ incapable of encryption. If verification failed, perhaps because the
+ message was also encrypted, sensible default values are returned.
"""
block_b = io.BytesIO(msg_block.encode('ascii'))
try:
sigs = gpgme_ctx.verify(block_b, None, plain_b)
except gpgme.GpgmeError:
- return ("",[],False)
+ return ("",[],False,False)
- plaintext = plain_b.getvalue().decode('utf-8')
+ plaintext_b = plain_b.getvalue()
- sigkey_missing = False
- fingerprints = []
- for sig in sigs:
- if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0):
- fingerprints += [sig.fpr]
- sigkey_missing = False
- break
- else:
- if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0):
- sigkey_missing = True
+ (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
- return (plaintext, fingerprints, sigkey_missing)
+ return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt)
def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
gpgme_ctx: the gpgme context
Returns:
- A tuple containging a list of signing fingerprints if the signature
- verification was sucessful, and a boolean marking whether edward is
- missing all public keys for validating any of the signatures.
- Otherwise, a tuple containing an empty list and True are returned.
+ A tuple containging a list of encryption capable signing fingerprints
+ if the signature verification was sucessful, a boolean marking whether
+ edward is missing all public keys for validating any of the signatures,
+ and a boolean marking whether all signing keys are incapable of
+ encryption. Otherwise, a tuple containing an empty list and True are
+ returned.
"""
detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
plaintext_fp = io.BytesIO(plaintext_bytes)
try:
- result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
+ sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
except gpgme.GpgmeError:
- return ([],False)
+ return ([],False,False)
- sigkey_missing = False
- sig_fingerprints = []
- for res_ in result:
- if (res_.summary == 0) or (res_.summary & gpgme.SIGSUM_VALID != 0) or (res_.summary & gpgme.SIGSUM_GREEN != 0):
- sig_fingerprints += [res_.fpr]
- sigkey_missing = False
- break
- else:
- if (res_.summary & gpgme.SIGSUM_KEY_MISSING != 0):
- sigkey_missing = True
+ (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
- return (sig_fingerprints, sigkey_missing)
+ return (fingerprints, sigkey_missing, key_cannot_encrypt)
def decrypt_block (msg_block, gpgme_ctx):
gpgme_ctx: the gpgme context
Returns:
- A tuple containing plaintext, signatures (if the decryption and
- signature verification were successful, respectively), and a boolean
- marking whether edward is missing all public keys for validating any of
- the signatures.
+ A tuple containing plaintext bytes, encryption-capable signatures (if
+ decryption and signature verification were successful, respectively), a
+ boolean marking whether edward is missing all public keys for
+ validating any of the signatures, and a boolean marking whether all
+ signature keys are incapable of encryption.
"""
block_b = io.BytesIO(msg_block.encode('ascii'))
try:
sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
except gpgme.GpgmeError:
- return ("",[],False)
+ return ("",[],False,False)
+
+ plaintext_b = plain_b.getvalue()
+
+ (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
+
+ return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt)
+
+
+def get_signature_fp (sigs, gpgme_ctx):
+ """Selects valid signatures from output of gpgme signature verifying functions
- plaintext = plain_b.getvalue().decode('utf-8')
+ get_signature_fp returns a list of valid signature fingerprints if those
+ fingerprints are associated with available keys capable of encryption.
+
+ Args:
+ sigs: a signature verification result object list
+ gpgme_ctx: a gpgme context
+
+ Returns:
+ fingerprints: a list of fingerprints
+ sigkey_missing: a boolean marking whether public keys are missing for
+ all available signatures.
+ key_cannot_encrypt: a boolearn marking whether available public keys are
+ incapable of encryption.
+ """
sigkey_missing = False
+ key_cannot_encrypt = False
fingerprints = []
+
for sig in sigs:
if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0):
- fingerprints += [sig.fpr]
- sigkey_missing = False
- break
- else:
+ try:
+ key_obj = gpgme_ctx.get_key(sig.fpr)
+ except:
+ if fingerprints == []:
+ sigkey_missing = True
+ continue
+
+ if is_key_usable(key_obj):
+ fingerprints += [sig.fpr]
+ key_cannot_encrypt = False
+ sigkey_missing = False
+
+ elif fingerprints == []:
+ key_cannot_encrypt = True
+
+ elif fingerprints == []:
if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0):
sigkey_missing = True
- return (plaintext, fingerprints, sigkey_missing)
+ return (fingerprints, sigkey_missing, key_cannot_encrypt)
-def email_to_from_subject (email_text):
+def is_key_usable (key_obj):
+ """Returns boolean representing key usability regarding encryption
+
+ Tests various feature of key and returns usability
+
+ Args:
+ key_obj: a gpgme key object
+
+ Returns:
+ A boolean representing key usability
+ """
+ if key_obj.can_encrypt and not key_obj.invalid and not key_obj.expired \
+ and not key_obj.revoked and not key_obj.disabled:
+ return True
+ else:
+ return False
+
+
+def email_to_from_subject (email_bytes):
"""Returns the values of the email's To:, From: and Subject: fields
Returns this information from an email.
Args:
- email_text: the string form of the email
+ email_bytes: the byte string form of the email
Returns:
the email To:, From:, and Subject: fields as strings
"""
- email_struct = email.parser.Parser().parsestr(email_text)
+ email_struct = email.parser.BytesParser().parsebytes(email_bytes)
email_to = email_struct['To']
email_from = email_struct['From']
A string version of the mime message, possibly encrypted and signed.
"""
- if (encrypt_to_key != None):
+ plaintext_mime = MIMEText(plaintext)
+ plaintext_mime.set_charset('utf-8')
- plaintext_mime = MIMEText(plaintext)
- plaintext_mime.set_charset('utf-8')
+ if (encrypt_to_key != None):
encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
encrypt_to_key,
message_mime['Content-Disposition'] = 'inline'
else:
- message_mime = MIMEText(plaintext)
- message_mime.set_charset('utf-8')
+ message_mime = plaintext_mime
message_mime['To'] = email_to
message_mime['Subject'] = email_subject