6aed6390b7ea7fc74d8bb365be5fc6647fe28022
1 #! /usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 """*********************************************************************
4 * Edward is free software: you can redistribute it and/or modify *
5 * it under the terms of the GNU Affero Public License as published by *
6 * the Free Software Foundation, either version 3 of the License, or *
7 * (at your option) any later version. *
9 * Edward is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU Affero Public License for more details. *
14 * You should have received a copy of the GNU Affero Public License *
15 * along with Edward. If not, see <http://www.gnu.org/licenses/>. *
17 * Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) *
18 * Copyright (C) 2014 Josh Drake (AGPLv3+) *
19 * Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) *
20 * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) *
21 * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) *
23 * Special thanks to Josh Drake for writing the original edward bot! :) *
25 ************************************************************************
27 Code sourced from these projects:
29 * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
30 * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
31 * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
45 from email
.mime
.multipart
import MIMEMultipart
46 from email
.mime
.application
import MIMEApplication
47 from email
.mime
.nonmultipart
import MIMENonMultipart
51 langs
= ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
53 """This list contains the abbreviated names of reply languages available to
57 match_types
= [('clearsign',
58 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
60 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
62 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
64 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
66 """This list of tuples matches query names with re.search() queries used
67 to find GPG data for edward to process."""
70 class EddyMsg (object):
72 The EddyMsg class represents relevant parts of a mime message.
74 The represented message can be single-part or multi-part.
76 'multipart' is set to True if there are multiple mime parts.
78 'subparts' points to a list of mime sub-parts if it is a multi-part
79 message. Otherwise it points to an empty list.
81 'payload_bytes' contains the raw mime-decoded bytes that haven't been
82 encoded into a character set.
84 'payload_pieces' is a list of objects containing strings that when strung
85 together form the fully-decoded string representation of the mime part.
87 The 'charset' describes the character set of payload_bytes.
89 The 'filename', 'content_type' and 'description_list' come from the mime
102 description_list
= None
105 class PayloadPiece (object):
107 PayloadPiece represents a complte or sub-section of a mime part.
109 Instances of this class are often strung together within one or more arrays
110 pointed to by each instance of the EddyMsg class.
112 'piece_type' refers to a string whose value describes the content of
113 'string'. Examples include "pubkey", for public keys, and "message", for
114 encrypted data (or armored signatures until they are known to be such.) The
115 names derive from the header and footer of each of these ascii-encoded gpg
118 'string' contains some string of text, such as non-GPG text, an encrypted
119 block of text, a signature, or a public key.
121 'gpg_data' points to any instances of GPGData that have been created based
122 on the contents of 'string'.
130 class GPGData (object):
132 GPGData holds info from decryption, sig. verification, and/or pub. keys.
134 Instances of this class contain decrypted information, signature
135 fingerprints and/or fingerprints of processed and imported public keys.
137 'decrypted' is set to True if 'plainobj' was created from encrypted data.
139 'plainobj' points to any decrypted, or signed part of, a GPG signature. It
140 is intended to be an instance of the EddyMsg class.
142 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
144 'keys' is a list of fingerprints of keys obtained in public key blocks.
154 class ReplyInfo (object):
156 ReplyInfo contains details that edward uses in generating its reply.
158 Instances of this class contain information about whether a message was
159 successfully encrypted or signed, and whether a public key was attached, or
160 retrievable, from the local GPG store. It stores the fingerprints of
161 potential encryption key candidates and the message (if any at all) to
162 quote in edward's reply.
164 'replies' points one of the dictionaries of translated replies.
166 'target_key' refers to the fingerprint of a key used to sign encrypted
167 data. This is the preferred key, if it is set, and if is available.
169 'fallback_target_key' referst to the fingerprint of a key used to sign
170 unencrypted data; alternatively it may be a public key attached to the
173 'msg_to_quote' refers to the part of a message which edward should quote in
174 his reply. This should remain as None if there was no encrypted and singed
175 part. This is to avoid making edward a service for decrypting other
176 people's messages to edward.
178 'success_decrypt' is set to True if edward could decrypt part of the
181 'failed_decrypt' is set to True if edward failed to decrypt part of the
184 'publick_key_received' is set to True if edward successfully imported a
187 'no_public_key' is set to True if edward doesn't have a key to encrypt to
188 when replying to the user.
190 'sig_success' is set to True if edward could to some extent verify the
191 signature of a signed part of the message to edward.
193 'sig_failure' is set to True if edward failed to some extent verify the
194 signature of a signed part of the message to edward.
200 fallback_target_key
= None
203 success_decrypt
= False
204 failed_decrypt
= False
205 public_key_received
= False
206 no_public_key
= False
214 This is the main function for edward, a GPG reply bot.
216 Edward responds to GPG-encrypted and signed mail, encrypting and signing
217 the response if the user's public key is, or was, included in the message.
226 Mime or plaintext email passing in through standard input. Portions of
227 the email may be encrypted. If the To: address contains the text
228 "edward-ja", then the reply will contain a reply written in the
229 Japanese language. There are other languages as well. The default
233 A reply email will be printed to standard output. The contents of the
234 reply email depends on whether the original email was encrypted or not,
235 has or doesn't have a signature, whether a public key used in the
236 original message is provided or locally stored, and the language
237 implied by the To: address in the original email.
242 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
243 edward_config
.sign_with_key
)
245 email_text
= sys
.stdin
.read()
246 email_struct
= parse_pgp_mime(email_text
, gpgme_ctx
)
248 email_to
, email_from
, email_subject
= email_to_from_subject(email_text
)
249 lang
= import_lang(email_to
)
251 replyinfo_obj
= ReplyInfo()
252 replyinfo_obj
.replies
= lang
.replies
254 prepare_for_reply(email_struct
, replyinfo_obj
)
255 encrypt_to_key
= get_key_from_fp(replyinfo_obj
, gpgme_ctx
)
256 reply_plaintext
= write_reply(replyinfo_obj
)
258 reply_mime
= generate_encrypted_mime(reply_plaintext
, email_from
, \
259 email_subject
, encrypt_to_key
,
265 def get_gpg_context (gnupghome
, sign_with_key_fp
):
267 os
.environ
['GNUPGHOME'] = gnupghome
269 gpgme_ctx
= gpgme
.Context()
270 gpgme_ctx
.armor
= True
273 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
275 error("unable to load signing key. is the gnupghome "
276 + "and signing key properly set in the edward_config.py?")
279 gpgme_ctx
.signers
= [sign_with_key
]
284 def parse_pgp_mime (email_text
, gpgme_ctx
):
286 email_struct
= email
.parser
.Parser().parsestr(email_text
)
288 eddymsg_obj
= parse_mime(email_struct
)
289 split_payloads(eddymsg_obj
)
290 gpg_on_payloads(eddymsg_obj
, gpgme_ctx
)
295 def parse_mime(msg_struct
):
297 eddymsg_obj
= EddyMsg()
299 if msg_struct
.is_multipart() == True:
300 payloads
= msg_struct
.get_payload()
302 eddymsg_obj
.multipart
= True
303 eddymsg_obj
.subparts
= list(map(parse_mime
, payloads
))
306 eddymsg_obj
= get_subpart_data(msg_struct
)
311 def scan_and_split (payload_piece
, match_type
, pattern
):
313 # don't try to re-split pieces containing gpg data
314 if payload_piece
.piece_type
!= "text":
315 return [payload_piece
]
317 flags
= re
.DOTALL | re
.MULTILINE
318 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
319 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
322 pieces
= [payload_piece
]
326 beginning
= PayloadPiece()
327 beginning
.string
= matches
.group('beginning')
328 beginning
.piece_type
= payload_piece
.piece_type
330 match
= PayloadPiece()
331 match
.string
= matches
.group('match')
332 match
.piece_type
= match_type
334 rest
= PayloadPiece()
335 rest
.string
= matches
.group('rest')
336 rest
.piece_type
= payload_piece
.piece_type
338 more_pieces
= scan_and_split(rest
, match_type
, pattern
)
339 pieces
= [beginning
, match
] + more_pieces
344 def get_subpart_data (part
):
348 obj
.charset
= part
.get_content_charset()
349 obj
.payload_bytes
= part
.get_payload(decode
=True)
351 obj
.filename
= part
.get_filename()
352 obj
.content_type
= part
.get_content_type()
353 obj
.description_list
= part
['content-description']
355 # your guess is as good as a-myy-ee-ine...
356 if obj
.charset
== None:
357 obj
.charset
= 'utf-8'
359 if obj
.payload_bytes
!= None:
361 payload
= PayloadPiece()
362 payload
.string
= obj
.payload_bytes
.decode(obj
.charset
)
363 payload
.piece_type
= 'text'
365 obj
.payload_pieces
= [payload
]
366 except UnicodeDecodeError:
372 def do_to_eddys_pieces (function_to_do
, eddymsg_obj
, data
):
374 if eddymsg_obj
.multipart
== True:
375 for sub
in eddymsg_obj
.subparts
:
376 do_to_eddys_pieces(function_to_do
, sub
, data
)
378 function_to_do(eddymsg_obj
, data
)
381 def split_payloads (eddymsg_obj
):
383 for match_type
in match_types
:
384 do_to_eddys_pieces(split_payload_pieces
, eddymsg_obj
, match_type
)
387 def split_payload_pieces (eddymsg_obj
, match_type
):
389 (match_name
, pattern
) = match_type
392 for piece
in eddymsg_obj
.payload_pieces
:
393 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
395 eddymsg_obj
.payload_pieces
= new_pieces_list
398 def gpg_on_payloads (eddymsg_obj
, gpgme_ctx
, prev_parts
=[]):
400 if eddymsg_obj
.multipart
== True:
402 for sub
in eddymsg_obj
.subparts
:
403 gpg_on_payloads (sub
, gpgme_ctx
, prev_parts
)
408 for piece
in eddymsg_obj
.payload_pieces
:
410 if piece
.piece_type
== "text":
411 # don't transform the plaintext.
414 elif piece
.piece_type
== "message":
415 (plaintext
, sigs
) = decrypt_block(piece
.string
, gpgme_ctx
)
418 piece
.gpg_data
= GPGData()
419 piece
.gpg_data
.decrypted
= True
420 piece
.gpg_data
.sigs
= sigs
422 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
425 # if not encrypted, check to see if this is an armored signature.
426 (plaintext
, sigs
) = verify_sig_message(piece
.string
, gpgme_ctx
)
429 piece
.piece_type
= "signature"
430 piece
.gpg_data
= GPGData()
431 piece
.gpg_data
.sigs
= sigs
433 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
435 elif piece
.piece_type
== "pubkey":
436 key_fps
= add_gpg_key(piece
.string
, gpgme_ctx
)
439 piece
.gpg_data
= GPGData()
440 piece
.gpg_data
.keys
= key_fps
442 elif piece
.piece_type
== "clearsign":
443 (plaintext
, sig_fps
) = verify_clear_signature(piece
.string
, gpgme_ctx
)
446 piece
.gpg_data
= GPGData()
447 piece
.gpg_data
.sigs
= sig_fps
448 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
450 elif piece
.piece_type
== "detachedsig":
451 for prev
in prev_parts
:
452 payload_bytes
= prev
.payload_bytes
453 sig_fps
= verify_detached_signature(piece
.string
, payload_bytes
, gpgme_ctx
)
456 piece
.gpg_data
= GPGData()
457 piece
.gpg_data
.sigs
= sig_fps
458 piece
.gpg_data
.plainobj
= prev
465 def prepare_for_reply (eddymsg_obj
, replyinfo_obj
):
467 do_to_eddys_pieces(prepare_for_reply_pieces
, eddymsg_obj
, replyinfo_obj
)
469 def prepare_for_reply_pieces (eddymsg_obj
, replyinfo_obj
):
471 for piece
in eddymsg_obj
.payload_pieces
:
472 if piece
.piece_type
== "text":
473 # don't quote the plaintext part.
476 elif piece
.piece_type
== "message":
477 prepare_for_reply_message(piece
, replyinfo_obj
)
479 elif piece
.piece_type
== "pubkey":
480 prepare_for_reply_pubkey(piece
, replyinfo_obj
)
482 elif (piece
.piece_type
== "clearsign") \
483 or (piece
.piece_type
== "detachedsig") \
484 or (piece
.piece_type
== "signature"):
485 prepare_for_reply_sig(piece
, replyinfo_obj
)
488 def prepare_for_reply_message (piece
, replyinfo_obj
):
490 if piece
.gpg_data
== None:
491 replyinfo_obj
.failed_decrypt
= True
494 replyinfo_obj
.success_decrypt
= True
496 # we already have a key (and a message)
497 if replyinfo_obj
.target_key
!= None:
500 if piece
.gpg_data
.sigs
!= []:
501 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
502 get_signed_part
= False
504 # only include a signed message in the reply.
505 get_signed_part
= True
507 replyinfo_obj
.msg_to_quote
= flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, get_signed_part
)
509 # to catch public keys in encrypted blocks
510 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
513 def prepare_for_reply_pubkey (piece
, replyinfo_obj
):
515 if piece
.gpg_data
== None or piece
.gpg_data
.keys
== []:
516 replyinfo_obj
.no_public_key
= True
518 replyinfo_obj
.public_key_received
= True
520 if replyinfo_obj
.fallback_target_key
== None:
521 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.keys
[0]
524 def prepare_for_reply_sig (piece
, replyinfo_obj
):
526 if piece
.gpg_data
== None or piece
.gpg_data
.sigs
== []:
527 replyinfo_obj
.sig_failure
= True
529 replyinfo_obj
.sig_success
= True
531 if replyinfo_obj
.fallback_target_key
== None:
532 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.sigs
[0]
536 def flatten_decrypted_payloads (eddymsg_obj
, get_signed_part
):
540 if eddymsg_obj
== None:
543 # recurse on multi-part mime
544 if eddymsg_obj
.multipart
== True:
545 for sub
in eddymsg_obj
.subparts
:
546 flat_string
+= flatten_decrypted_payloads (sub
, get_signed_part
)
550 for piece
in eddymsg_obj
.payload_pieces
:
551 if piece
.piece_type
== "text":
552 flat_string
+= piece
.string
554 if (get_signed_part
):
555 # don't include nested encryption
556 if (piece
.piece_type
== "message") \
557 and (piece
.gpg_data
!= None) \
558 and (piece
.gpg_data
.decrypted
== False):
559 flat_string
+= flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, get_signed_part
)
561 elif ((piece
.piece_type
== "clearsign") \
562 or (piece
.piece_type
== "detachedsig") \
563 or (piece
.piece_type
== "signature")) \
564 and (piece
.gpg_data
!= None):
565 # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
566 flat_string
+= flatten_decrypted_payloads (piece
.gpg_data
.plainobj
, get_signed_part
)
571 def get_key_from_fp (replyinfo_obj
, gpgme_ctx
):
573 if replyinfo_obj
.target_key
== None:
574 replyinfo_obj
.target_key
= replyinfo_obj
.fallback_target_key
576 if replyinfo_obj
.target_key
!= None:
578 encrypt_to_key
= gpgme_ctx
.get_key(replyinfo_obj
.target_key
)
579 return encrypt_to_key
584 # no available key to use
585 replyinfo_obj
.target_key
= None
586 replyinfo_obj
.fallback_target_key
= None
588 replyinfo_obj
.no_public_key
= True
589 replyinfo_obj
.public_key_received
= False
594 def write_reply (replyinfo_obj
):
598 if replyinfo_obj
.success_decrypt
== True:
599 reply_plain
+= replyinfo_obj
.replies
['success_decrypt']
601 if replyinfo_obj
.no_public_key
== False:
602 quoted_text
= email_quote_text(replyinfo_obj
.msg_to_quote
)
603 reply_plain
+= quoted_text
605 elif replyinfo_obj
.failed_decrypt
== True:
606 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
609 if replyinfo_obj
.sig_success
== True:
610 reply_plain
+= "\n\n"
611 reply_plain
+= replyinfo_obj
.replies
['sig_success']
613 elif replyinfo_obj
.sig_failure
== True:
614 reply_plain
+= "\n\n"
615 reply_plain
+= replyinfo_obj
.replies
['sig_failure']
618 if replyinfo_obj
.public_key_received
== True:
619 reply_plain
+= "\n\n"
620 reply_plain
+= replyinfo_obj
.replies
['public_key_received']
622 elif replyinfo_obj
.no_public_key
== True:
623 reply_plain
+= "\n\n"
624 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
627 reply_plain
+= "\n\n"
628 reply_plain
+= replyinfo_obj
.replies
['signature']
633 def add_gpg_key (key_block
, gpgme_ctx
):
635 fp
= io
.BytesIO(key_block
.encode('ascii'))
637 result
= gpgme_ctx
.import_(fp
)
638 imports
= result
.imports
640 key_fingerprints
= []
643 for import_
in imports
:
644 fingerprint
= import_
[0]
645 key_fingerprints
+= [fingerprint
]
647 debug("added gpg key: " + fingerprint
)
649 return key_fingerprints
652 def verify_sig_message (msg_block
, gpgme_ctx
):
654 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
655 plain_b
= io
.BytesIO()
658 sigs
= gpgme_ctx
.verify(block_b
, None, plain_b
)
662 plaintext
= plain_b
.getvalue().decode('utf-8')
666 fingerprints
+= [sig
.fpr
]
667 return (plaintext
, fingerprints
)
670 def verify_clear_signature (sig_block
, gpgme_ctx
):
672 # FIXME: this might require the un-decoded bytes
673 # or the correct re-encoding with the carset of the mime part.
674 msg_fp
= io
.BytesIO(sig_block
.encode('utf-8'))
675 ptxt_fp
= io
.BytesIO()
677 result
= gpgme_ctx
.verify(msg_fp
, None, ptxt_fp
)
679 # FIXME: this might require using the charset of the mime part.
680 plaintext
= ptxt_fp
.getvalue().decode('utf-8')
682 sig_fingerprints
= []
684 sig_fingerprints
+= [res_
.fpr
]
686 return plaintext
, sig_fingerprints
689 def verify_detached_signature (detached_sig
, plaintext_bytes
, gpgme_ctx
):
691 detached_sig_fp
= io
.BytesIO(detached_sig
.encode('ascii'))
692 plaintext_fp
= io
.BytesIO(plaintext_bytes
)
693 ptxt_fp
= io
.BytesIO()
695 result
= gpgme_ctx
.verify(detached_sig_fp
, plaintext_fp
, None)
697 sig_fingerprints
= []
699 sig_fingerprints
+= [res_
.fpr
]
701 return sig_fingerprints
704 def decrypt_block (msg_block
, gpgme_ctx
):
706 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
707 plain_b
= io
.BytesIO()
710 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
714 plaintext
= plain_b
.getvalue().decode('utf-8')
718 fingerprints
+= [sig
.fpr
]
719 return (plaintext
, fingerprints
)
722 def choose_reply_encryption_key (gpgme_ctx
, fingerprints
):
725 for fp
in fingerprints
:
727 key
= gpgme_ctx
.get_key(fp
)
729 if (key
.can_encrypt
== True):
739 def email_to_from_subject (email_text
):
741 email_struct
= email
.parser
.Parser().parsestr(email_text
)
743 email_to
= email_struct
['To']
744 email_from
= email_struct
['From']
745 email_subject
= email_struct
['Subject']
747 return email_to
, email_from
, email_subject
750 def import_lang(email_to
):
754 if "edward-" + lang
in email_to
:
755 lang
= "lang." + re
.sub('-', '_', lang
)
756 language
= importlib
.import_module(lang
)
760 return importlib
.import_module("lang.en")
763 def generate_encrypted_mime (plaintext
, email_from
, email_subject
, encrypt_to_key
,
766 # quoted printable encoding lets most ascii characters look normal
767 # before the mime message is decoded.
768 char_set
= email
.charset
.Charset("utf-8")
769 char_set
.body_encoding
= email
.charset
.QP
771 # MIMEText doesn't allow setting the text encoding
772 # so we use MIMENonMultipart.
773 plaintext_mime
= MIMENonMultipart('text', 'plain')
774 plaintext_mime
.set_payload(plaintext
, charset
=char_set
)
776 if (encrypt_to_key
!= None):
778 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
782 control_mime
= MIMEApplication("Version: 1",
783 _subtype
='pgp-encrypted',
784 _encoder
=email
.encoders
.encode_7or8bit
)
785 control_mime
['Content-Description'] = 'PGP/MIME version identification'
786 control_mime
.set_charset('us-ascii')
788 encoded_mime
= MIMEApplication(encrypted_text
,
789 _subtype
='octet-stream; name="encrypted.asc"',
790 _encoder
=email
.encoders
.encode_7or8bit
)
791 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
792 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
793 encoded_mime
.set_charset('us-ascii')
795 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
796 message_mime
.attach(control_mime
)
797 message_mime
.attach(encoded_mime
)
798 message_mime
['Content-Disposition'] = 'inline'
801 message_mime
= plaintext_mime
803 message_mime
['To'] = email_from
804 message_mime
['Subject'] = email_subject
806 reply
= message_mime
.as_string()
811 def email_quote_text (text
):
813 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
815 return quoted_message
818 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
820 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
821 encrypted_bytes
= io
.BytesIO()
823 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
824 plaintext_bytes
, encrypted_bytes
)
826 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
830 def error (error_msg
):
832 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
835 def debug (debug_msg
):
837 if edward_config
.debug
== True:
842 if __name__
== "__main__":
845 progname
= sys
.argv
[0]
847 if len(sys
.argv
) > 1:
848 print(progname
+ ": error, this program doesn't " \
849 "need any arguments.", file=sys
.stderr
)