pick out encryption keys from nested signed text
[edward.git] / edward
diff --git a/edward b/edward
index 5f61c29b11dc5ee46024bfcc2d06a5e1d3878610..a7c7874549663263ff68d8f3120add81ee2e6ad5 100755 (executable)
--- a/edward
+++ b/edward
@@ -729,7 +729,7 @@ def prepare_for_reply_message (piece, replyinfo_obj):
         # only include a signed message in the reply.
         get_signed_part = True
 
-    replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
+    flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
 
     # to catch public keys in encrypted blocks
     prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
@@ -786,39 +786,44 @@ def prepare_for_reply_sig (piece, replyinfo_obj):
             replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
 
 
+def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
+    """For creating a string representation of a signed, encrypted part.
 
-def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
-    """Returns a string representation of a signed, encrypted part.
-
-    Returns the string representation of the first signed/encrypted or signed
-    then encrypted block of text. (Signature inside of Encryption)
+    When given a decrypted payload, it will add either the plaintext or signed
+    plaintext to the reply message, depeding on 'get_signed_part'. This is
+    useful for ensuring that the reply message only comes from a signed and
+    ecrypted GPG message. It also sets the target_key for encrypting the reply
+    if it's told to get signed text only.
 
     Args:
         eddymsg_obj: the message in EddyMsg format created by decrypting GPG
             text
+        replyinfo_obj: a ReplyInfo object for holding the message to quote and
+            the target_key to encrypt to.
         get_signed_part: True if we should only include text that contains a
             further signature. If False, then include plain text.
 
     Returns:
-        A string representation of encrypted and signed text.
+        Nothing
 
     Pre:
         The EddyMsg instance passed in should be a piece.gpg_data.plainobj
         which represents decrypted text. It may or may not be signed on that
         level.
-    """
 
-    flat_string = ""
+    Post:
+        the ReplyInfo instance may have a new 'target_key' set and its
+        'msg_to_quote' will be updated with (possibly signed) plaintext, if any
+        could be found.
+    """
 
     if eddymsg_obj == None:
-        return ""
+        return
 
     # recurse on multi-part mime
     if eddymsg_obj.multipart == True:
         for sub in eddymsg_obj.subparts:
-            flat_string += flatten_decrypted_payloads (sub, get_signed_part)
-
-        return flat_string
+            flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
 
     for piece in eddymsg_obj.payload_pieces:
         if (get_signed_part):
@@ -826,14 +831,12 @@ def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
                     or (piece.piece_type == "detachedsig") \
                     or (piece.piece_type == "signature")) \
                     and (piece.gpg_data != None):
-                        # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
-                        flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, False)
+                        flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False)
+                        replyinfo_obj.target_key = piece.gpg_data.sigs[0]
                         break
         else:
             if piece.piece_type == "text":
-                flat_string += piece.string
-
-    return flat_string
+                replyinfo_obj.msg_to_quote += piece.string
 
 
 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
@@ -1093,10 +1096,17 @@ def decrypt_block (msg_block, gpgme_ctx):
     return (plaintext, fingerprints)
 
 
+def email_to_from_subject (email_text):
+    """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
 
-def email_to_from_subject (email_text):
+    Returns:
+        the email To:, From:, and Subject: fields as strings
+    """
 
     email_struct = email.parser.Parser().parsestr(email_text)
 
@@ -1108,6 +1118,21 @@ def email_to_from_subject (email_text):
 
 
 def import_lang(email_to):
+    """Imports appropriate language file for basic i18n support
+
+    The language imported depends on the To: address of the email received by
+    edward. an -en ending implies the English language, whereas a -ja ending
+    implies Japanese. The list of supported languages is listed in the 'langs'
+    list at the beginning of the program.
+
+    Args:
+        email_to: the string containing the email address that the mail was
+        sent to.
+
+    Returns:
+        the reference to the imported language module. The only variable in
+        this file is the 'replies' dictionary.
+    """
 
     if email_to != None:
         for lang in langs:
@@ -1122,6 +1147,22 @@ def import_lang(email_to):
 
 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
                     gpgme_ctx):
+    """This function creates the mime email reply. It can encrypt the email.
+
+    If the encrypt_key is included, then the email is encrypted and signed.
+    Otherwise it is unencrypted.
+
+    Args:
+        plaintext: the plaintext body of the message to create.
+        email_from: the email address to reply to
+        email_subject: the subject to use in reply
+        encrypt_to_key: the key object to use for encrypting the email. (or
+            None)
+        gpgme_ctx: the gpgme context
+
+    Returns
+        A string version of the mime message, possibly encrypted and signed.
+    """
 
     # quoted printable encoding lets most ascii characters look normal
     # before the mime message is decoded.
@@ -1138,27 +1179,30 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
         encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
                                               encrypt_to_key,
                                               gpgme_ctx)
+        gpg_payload = encrypted_text
+
+    else:
+        signed_text = sign_message(plaintext_mime.as_string(), gpgme_ctx)
+        gpg_payload = signed_text
 
-        control_mime = MIMEApplication("Version: 1",
-                                       _subtype='pgp-encrypted',
-                                       _encoder=email.encoders.encode_7or8bit)
-        control_mime['Content-Description'] = 'PGP/MIME version identification'
-        control_mime.set_charset('us-ascii')
+    control_mime = MIMEApplication("Version: 1",
+                                   _subtype='pgp-encrypted',
+                                   _encoder=email.encoders.encode_7or8bit)
+    control_mime['Content-Description'] = 'PGP/MIME version identification'
+    control_mime.set_charset('us-ascii')
 
-        encoded_mime = MIMEApplication(encrypted_text,
-                                       _subtype='octet-stream; name="encrypted.asc"',
-                                       _encoder=email.encoders.encode_7or8bit)
-        encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
-        encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
-        encoded_mime.set_charset('us-ascii')
+    encoded_mime = MIMEApplication(gpg_payload,
+                                   _subtype='octet-stream; name="encrypted.asc"',
+                                   _encoder=email.encoders.encode_7or8bit)
+    encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
+    encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
+    encoded_mime.set_charset('us-ascii')
 
-        message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
-        message_mime.attach(control_mime)
-        message_mime.attach(encoded_mime)
-        message_mime['Content-Disposition'] = 'inline'
+    message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
+    message_mime.attach(control_mime)
+    message_mime.attach(encoded_mime)
+    message_mime['Content-Disposition'] = 'inline'
 
-    else:
-        message_mime = plaintext_mime
 
     message_mime['To'] = email_from
     message_mime['Subject'] = email_subject
@@ -1169,6 +1213,17 @@ def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_ke
 
 
 def email_quote_text (text):
+    """Quotes input text by inserting "> "s
+
+    This is useful for quoting a text for the reply message. It inserts "> "
+    strings at the beginning of lines.
+
+    Args:
+        text: plain text to quote
+
+    Returns:
+        Quoted text
+    """
 
     quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
 
@@ -1176,7 +1231,20 @@ def email_quote_text (text):
 
 
 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
+    """Encrypts and signs plaintext
+
+    This encrypts and signs a message.
+
+    Args:
+        plaintext: text to sign and ecrypt
+        encrypt_to_key: the key object to encrypt to
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        An encrypted and signed string of text
+    """
 
+    # the plaintext should be mime encoded in an ascii-compatible form
     plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
     encrypted_bytes = io.BytesIO()
 
@@ -1187,18 +1255,83 @@ def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
     return encrypted_txt
 
 
+def sign_message (plaintext, gpgme_ctx):
+    """Signs plaintext
+
+    This signs a message.
+
+    Args:
+        plaintext: text to sign
+        gpgme_ctx: the gpgme context
+
+    Returns:
+        An armored signature as a string of text
+    """
+
+    # the plaintext should be mime encoded in an ascii-compatible form
+    plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
+    signed_bytes = io.BytesIO()
+
+    gpgme_ctx.sign(plaintext_bytes, signed_bytes, gpgme.SIG_MODE_NORMAL)
+
+    signed_txt = signed_bytes.getvalue().decode('ascii')
+    return signed_txt
+
+
 def error (error_msg):
+    """Write an error message to stdout
+
+    The error message includes the program name.
+
+    Args:
+        error_msg: the message to print
+
+    Returns:
+        Nothing
+
+    Post:
+        An error message is printed to stdout
+    """
 
     sys.stderr.write(progname + ": " + str(error_msg) + "\n")
 
 
 def debug (debug_msg):
+    """Writes a debug message to stdout if debug == True
+
+    If the debug option is set in edward_config.py, then the passed message
+    gets printed to stdout.
+
+    Args:
+        debug_msg: the message to print to stdout
+
+    Returns:
+        Nothing
+
+    Post:
+        A debug message is printed to stdout
+    """
 
     if edward_config.debug == True:
         error(debug_msg)
 
 
 def handle_args ():
+    """Sets the progname variable and complains about any arguments
+
+    If there are any arguments, then edward complains and quits, because input
+    is read from stdin.
+
+    Args:
+        None
+
+    Returns:
+        None
+
+    Post:
+        Exits with error 1 if there are arguments, otherwise returns to the
+        calling function, such as main().
+    """
 
     global progname
     progname = sys.argv[0]
@@ -1210,6 +1343,7 @@ def handle_args ():
 
 
 if __name__ == "__main__":
+    """Executes main if this file is not loaded interactively"""
 
     main()