1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
3 # This file is part of pgp-mime.
5 # pgp-mime is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # pgp-mime is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # pgp-mime. If not, see <http://www.gnu.org/licenses/>.
18 from email import message_from_bytes as _message_from_bytes
19 from email.encoders import encode_7or8bit as _encode_7or8bit
20 from email.mime.application import MIMEApplication as _MIMEApplication
21 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
23 from . import LOG as _LOG
24 from .crypt import sign_and_encrypt_bytes as _sign_and_encrypt_bytes
25 from .crypt import verify_bytes as _verify_bytes
26 from .email import email_targets as _email_targets
27 from .email import strip_bcc as _strip_bcc
30 def sign(message, signers=None, **kwargs):
31 r"""Sign a ``Message``, returning the signed version.
35 +-> application/pgp-signature (signature)
37 >>> from pgp_mime.email import encodedMIMEText
38 >>> message = encodedMIMEText('Hi\nBye')
39 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
40 >>> signed.set_boundary('boundsep')
41 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
42 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
44 Content-Disposition: inline
47 Content-Type: text/plain; charset="us-ascii"
49 Content-Transfer-Encoding: 7bit
50 Content-Disposition: inline
56 Content-Transfer-Encoding: 7bit
57 Content-Description: OpenPGP digital signature
58 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
60 -----BEGIN PGP SIGNATURE-----
62 -----END PGP SIGNATURE-----
66 >>> from email.mime.multipart import MIMEMultipart
67 >>> message = MIMEMultipart()
68 >>> message.attach(encodedMIMEText('Part A'))
69 >>> message.attach(encodedMIMEText('Part B'))
70 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
71 >>> signed.set_boundary('boundsep')
72 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
73 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
75 Content-Disposition: inline
78 Content-Type: multipart/mixed; boundary="===============...=="
81 --===============...==
82 Content-Type: text/plain; charset="us-ascii"
84 Content-Transfer-Encoding: 7bit
85 Content-Disposition: inline
88 --===============...==
89 Content-Type: text/plain; charset="us-ascii"
91 Content-Transfer-Encoding: 7bit
92 Content-Disposition: inline
95 --===============...==--
98 Content-Transfer-Encoding: 7bit
99 Content-Description: OpenPGP digital signature
100 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
102 -----BEGIN PGP SIGNATURE-----
104 -----END PGP SIGNATURE-----
108 body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
109 # use email.policy.SMTP once we get Python 3.3
110 signature = str(_sign_and_encrypt_bytes(data=body, **kwargs), 'us-ascii')
111 sig = _MIMEApplication(
113 _subtype='pgp-signature; name="signature.asc"',
114 _encoder=_encode_7or8bit)
115 sig['Content-Description'] = 'OpenPGP digital signature'
116 sig.set_charset('us-ascii')
118 msg = _MIMEMultipart(
119 'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
122 msg['Content-Disposition'] = 'inline'
125 def encrypt(message, recipients=None, **kwargs):
126 r"""Encrypt a ``Message``, returning the encrypted version.
129 +-> application/pgp-encrypted (control information)
130 +-> application/octet-stream (body)
132 >>> from pgp_mime.email import encodedMIMEText
133 >>> message = encodedMIMEText('Hi\nBye')
134 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
135 >>> encrypted = encrypt(message)
136 >>> encrypted.set_boundary('boundsep')
137 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
138 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
140 Content-Disposition: inline
144 Content-Transfer-Encoding: 7bit
145 Content-Type: application/pgp-encrypted; charset="us-ascii"
151 Content-Transfer-Encoding: 7bit
152 Content-Description: OpenPGP encrypted message
153 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
155 -----BEGIN PGP MESSAGE-----
157 -----END PGP MESSAGE-----
161 >>> from email.mime.multipart import MIMEMultipart
162 >>> message = MIMEMultipart()
163 >>> message.attach(encodedMIMEText('Part A'))
164 >>> message.attach(encodedMIMEText('Part B'))
165 >>> encrypted = encrypt(
166 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
167 >>> encrypted.set_boundary('boundsep')
168 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
169 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
171 Content-Disposition: inline
175 Content-Transfer-Encoding: 7bit
176 Content-Type: application/pgp-encrypted; charset="us-ascii"
182 Content-Transfer-Encoding: 7bit
183 Content-Description: OpenPGP encrypted message
184 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
186 -----BEGIN PGP MESSAGE-----
188 -----END PGP MESSAGE-----
192 body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
193 # use email.policy.SMTP once we get Python 3.3
194 if recipients is None:
195 recipients = [email for name,email in _email_targets(message)]
196 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
197 encrypted = str(_sign_and_encrypt_bytes(
198 data=body, recipients=recipients, **kwargs), 'us-ascii')
199 enc = _MIMEApplication(
201 _subtype='octet-stream; name="encrypted.asc"',
202 _encoder=_encode_7or8bit)
203 enc['Content-Description'] = 'OpenPGP encrypted message'
204 enc.set_charset('us-ascii')
205 control = _MIMEApplication(
206 _data='Version: 1\n',
207 _subtype='pgp-encrypted',
208 _encoder=_encode_7or8bit)
209 control.set_charset('us-ascii')
210 msg = _MIMEMultipart(
213 protocol='application/pgp-encrypted')
216 msg['Content-Disposition'] = 'inline'
219 def sign_and_encrypt(message, signers=None, recipients=None, **kwargs):
220 r"""Sign and encrypt a ``Message``, returning the encrypted version.
223 +-> application/pgp-encrypted (control information)
224 +-> application/octet-stream (body)
226 >>> from pgp_mime.email import encodedMIMEText
227 >>> message = encodedMIMEText('Hi\nBye')
228 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
229 >>> encrypted = sign_and_encrypt(
230 ... message, signers=['pgp-mime@invalid.com'], always_trust=True)
231 >>> encrypted.set_boundary('boundsep')
232 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
233 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
235 Content-Disposition: inline
239 Content-Transfer-Encoding: 7bit
240 Content-Type: application/pgp-encrypted; charset="us-ascii"
246 Content-Transfer-Encoding: 7bit
247 Content-Description: OpenPGP encrypted message
248 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
250 -----BEGIN PGP MESSAGE-----
252 -----END PGP MESSAGE-----
256 >>> from email.mime.multipart import MIMEMultipart
257 >>> message = MIMEMultipart()
258 >>> message.attach(encodedMIMEText('Part A'))
259 >>> message.attach(encodedMIMEText('Part B'))
260 >>> encrypted = sign_and_encrypt(
261 ... message, signers=['pgp-mime@invalid.com'],
262 ... recipients=['pgp-mime@invalid.com'], always_trust=True)
263 >>> encrypted.set_boundary('boundsep')
264 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
265 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
267 Content-Disposition: inline
271 Content-Transfer-Encoding: 7bit
272 Content-Type: application/pgp-encrypted; charset="us-ascii"
278 Content-Transfer-Encoding: 7bit
279 Content-Description: OpenPGP encrypted message
280 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
282 -----BEGIN PGP MESSAGE-----
284 -----END PGP MESSAGE-----
288 _strip_bcc(message=message)
289 body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
290 # use email.policy.SMTP once we get Python 3.3
291 if recipients is None:
292 recipients = [email for name,email in _email_targets(message)]
293 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
295 _sign_and_encrypt_bytes(
296 data=body, signers=signers, recipients=recipients, **kwargs),
298 enc = _MIMEApplication(
300 _subtype='octet-stream; name="encrypted.asc"',
301 _encoder=_encode_7or8bit)
302 enc['Content-Description'] = 'OpenPGP encrypted message'
303 enc.set_charset('us-ascii')
304 control = _MIMEApplication(
305 _data='Version: 1\n',
306 _subtype='pgp-encrypted',
307 _encoder=_encode_7or8bit)
308 control.set_charset('us-ascii')
309 msg = _MIMEMultipart(
312 protocol='application/pgp-encrypted')
315 msg['Content-Disposition'] = 'inline'
318 def _get_encrypted_parts(message):
319 ct = message.get_content_type()
320 assert ct == 'multipart/encrypted', ct
321 params = dict(message.get_params())
322 assert params.get('protocol', None) == 'application/pgp-encrypted', params
323 assert message.is_multipart(), message
324 control = body = None
325 for part in message.get_payload():
328 assert part.is_multipart() == False, part
329 ct = part.get_content_type()
330 if ct == 'application/pgp-encrypted':
332 raise ValueError('multiple application/pgp-encrypted parts')
334 elif ct == 'application/octet-stream':
336 raise ValueError('multiple application/octet-stream parts')
339 raise ValueError('unnecessary {} part'.format(ct))
341 raise ValueError('missing application/pgp-encrypted part')
343 raise ValueError('missing application/octet-stream part')
344 return (control, body)
346 def _get_signed_parts(message):
347 ct = message.get_content_type()
348 assert ct == 'multipart/signed', ct
349 params = dict(message.get_params())
350 assert params.get('protocol', None) == 'application/pgp-signature', params
351 assert message.is_multipart(), message
352 body = signature = None
353 for part in message.get_payload():
356 ct = part.get_content_type()
357 if ct == 'application/pgp-signature':
359 raise ValueError('multiple application/pgp-signature parts')
363 raise ValueError('multiple non-signature parts')
366 raise ValueError('missing body part')
368 raise ValueError('missing application/pgp-signature part')
369 return (body, signature)
371 def decrypt(message, **kwargs):
372 r"""Decrypt a multipart/encrypted message.
374 >>> from pgp_mime.email import encodedMIMEText
375 >>> message = encodedMIMEText('Hi\nBye')
376 >>> encrypted = encrypt(message, recipients=['<pgp-mime@invalid.com>'])
377 >>> decrypted = decrypt(encrypted)
378 >>> print(decrypted.as_string().replace('\r\n', '\n'))
379 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
380 Content-Type: text/plain; charset="us-ascii"
382 Content-Transfer-Encoding: 7bit
383 Content-Disposition: inline
388 >>> from email.mime.multipart import MIMEMultipart
389 >>> message = MIMEMultipart()
390 >>> message.attach(encodedMIMEText('Part A'))
391 >>> message.attach(encodedMIMEText('Part B'))
392 >>> encrypted = encrypt(
393 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
394 >>> decrypted = decrypt(encrypted)
395 >>> decrypted.set_boundary('boundsep')
396 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
397 Content-Type: multipart/mixed; boundary="boundsep"
401 Content-Type: text/plain; charset="us-ascii"
403 Content-Transfer-Encoding: 7bit
404 Content-Disposition: inline
408 Content-Type: text/plain; charset="us-ascii"
410 Content-Transfer-Encoding: 7bit
411 Content-Disposition: inline
417 control,body = _get_encrypted_parts(message)
418 encrypted = body.get_payload(decode=True)
419 if not isinstance(encrypted, bytes):
420 encrypted = encrypted.encode('us-ascii')
421 decrypted,verified,result = _verify_bytes(encrypted, **kwargs)
422 return _message_from_bytes(decrypted)
424 def verify(message, **kwargs):
425 r"""Verify a signature on ``message``, possibly decrypting first.
427 >>> from pgp_mime.email import encodedMIMEText
428 >>> message = encodedMIMEText('Hi\nBye')
429 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
430 >>> encrypted = sign_and_encrypt(message, signers=['pgp-mime@invalid.com'],
431 ... always_trust=True)
432 >>> decrypted,verified,signatures = verify(encrypted)
433 >>> print(decrypted.as_string().replace('\r\n', '\n'))
434 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
435 Content-Type: text/plain; charset="us-ascii"
437 Content-Transfer-Encoding: 7bit
438 Content-Disposition: inline
439 To: pgp-mime-test <pgp-mime@invalid.com>
445 >>> for s in signatures:
446 ... print(s.dumps()) # doctest: +REPORT_UDIFF
447 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
448 DECC812C8795ADD60538B0CD171008BA2F73DE2E signature:
458 signature expired: False
463 expiration timestamp: None
464 wrong key usage: False
465 pka trust: not available
468 validity reason: success
469 public key algorithm: RSA
470 hash algorithm: SHA256
472 >>> from email.mime.multipart import MIMEMultipart
473 >>> message = MIMEMultipart()
474 >>> message.attach(encodedMIMEText('Part A'))
475 >>> message.attach(encodedMIMEText('Part B'))
476 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
477 >>> decrypted,verified,signatures = verify(signed)
478 >>> decrypted.set_boundary('boundsep')
479 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
480 Content-Type: multipart/mixed; boundary="boundsep"
484 Content-Type: text/plain; charset="us-ascii"
486 Content-Transfer-Encoding: 7bit
487 Content-Disposition: inline
491 Content-Type: text/plain; charset="us-ascii"
493 Content-Transfer-Encoding: 7bit
494 Content-Disposition: inline
500 >>> for s in signatures:
501 ... print(s.dumps()) # doctest: +REPORT_UDIFF
502 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
503 DECC812C8795ADD60538B0CD171008BA2F73DE2E signature:
513 signature expired: False
518 expiration timestamp: None
519 wrong key usage: False
520 pka trust: not available
523 validity reason: success
524 public key algorithm: RSA
527 Test a message generated by Mutt (for sanity):
529 >>> from email import message_from_bytes
530 >>> message_bytes = b'\n'.join([
531 ... b'Return-Path: <pgp-mime@invalid.com>',
532 ... b'Received: by invalid; Tue, 24 Apr 2012 19:46:59 -0400',
533 ... b'Date: Tue, 24 Apr 2012 19:46:59 -0400',
534 ... b'From: pgp-mime-test <pgp-mime@invalid.com',
535 ... b'To: pgp-mime@invalid.com',
536 ... b'Subject: test',
537 ... b'Message-ID: <20120424233415.GA27788@invalid>',
538 ... b'MIME-Version: 1.0',
539 ... b'Content-Type: multipart/signed; micalg=pgp-sha1;',
540 ... b' protocol="application/pgp-signature";',
541 ... b' boundary="kORqDWCi7qDJ0mEj"',
542 ... b'Content-Disposition: inline',
543 ... b'User-Agent: Mutt/1.5.21 (2010-09-15)',
544 ... b'Content-Length: 740',
547 ... b'--kORqDWCi7qDJ0mEj',
548 ... b'Content-Type: text/plain; charset=us-ascii',
549 ... b'Content-Disposition: inline',
553 ... b'--kORqDWCi7qDJ0mEj',
554 ... b'Content-Type: application/pgp-signature; name="signature.asc"',
555 ... b'Content-Description: OpenPGP digital signature',
557 ... b'-----BEGIN PGP SIGNATURE-----',
558 ... b'Version: GnuPG v2.0.17 (GNU/Linux)',
560 ... b'iQEcBAEBAgAGBQJPlztxAAoJEFEa7aZDMrbjwT0H/i9eN6CJ2FIinK7Ps04XYEbL',
561 ... b'PSQV1xCxb+2bk7yA4zQnjAKOPSuMDXfVG669Pbj8yo4DOgUqIgh+lK+voec9uwsJ',
562 ... b'ZgUJcMozSmEFSTPO+Fiyx0S+NjnaLsas6IQrQTVDc6lWiIZttgxuN0crH5DcLomB',
563 ... b'Ip90+ELbzVN3yBAjMJ1Y6xnKd7C0IOKm7VunYu9eCzJ/Rik5qZ0+IacQQnnrFJEN',
564 ... b'04nDvDUzfaKy80Ke7VAQBIRi85XCsM2h0KDXOGUZ0xPQ8L/4eUK9tL6DJaqKqFPl',
565 ... b'zNiwfpue01o6l6kngrQdXZ3tuv0HbLGc4ACzfz5XuGvE5PYTNEsylKLUMiSCIFc=',
567 ... b'-----END PGP SIGNATURE-----',
569 ... b'--kORqDWCi7qDJ0mEj--',
571 >>> message = message_from_bytes(message_bytes)
572 >>> decrypted,verified,signatures = verify(message)
573 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
574 Content-Type: text/plain; charset=us-ascii
575 Content-Disposition: inline
581 >>> for s in signatures:
582 ... print(s.dumps()) # doctest: +REPORT_UDIFF
583 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
584 B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3 signature:
594 signature expired: False
598 timestamp: Tue Apr 24 23:46:57 2012
599 expiration timestamp: None
600 wrong key usage: False
601 pka trust: not available
604 validity reason: success
605 public key algorithm: RSA
608 ct = message.get_content_type()
609 if ct == 'multipart/encrypted':
610 control,body = _get_encrypted_parts(message)
611 encrypted = body.get_payload(decode=True)
612 if not isinstance(encrypted, bytes):
613 encrypted = encrypted.encode('us-ascii')
614 decrypted,verified,message = _verify_bytes(encrypted)
615 return (_message_from_bytes(decrypted), verified, message)
616 body,signature = _get_signed_parts(message)
617 sig_data = signature.get_payload(decode=True)
618 if not isinstance(sig_data, bytes):
619 sig_data = sig_data.encode('us-ascii')
620 decrypted,verified,result = _verify_bytes(
621 body.as_string().encode('us-ascii').replace(b'\n', b'\r\n'),
622 signature=sig_data, **kwargs)
623 # use email.policy.SMTP once we get Python 3.3
624 return (_copy.deepcopy(body), verified, result)