3 from email import message_from_bytes as _message_from_bytes
4 from email.encoders import encode_7or8bit as _encode_7or8bit
5 from email.mime.application import MIMEApplication as _MIMEApplication
6 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
8 from . import LOG as _LOG
9 from .crypt import sign_and_encrypt_bytes as _sign_and_encrypt_bytes
10 from .crypt import verify_bytes as _verify_bytes
11 from .email import email_targets as _email_targets
12 from .email import strip_bcc as _strip_bcc
15 def sign(message, signers=None):
16 r"""Sign a ``Message``, returning the signed version.
20 +-> application/pgp-signature (signature)
22 >>> from pgp_mime.email import encodedMIMEText
23 >>> message = encodedMIMEText('Hi\nBye')
24 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
25 >>> signed.set_boundary('boundsep')
26 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
27 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
29 Content-Disposition: inline
32 Content-Type: text/plain; charset="us-ascii"
34 Content-Transfer-Encoding: 7bit
35 Content-Disposition: inline
41 Content-Transfer-Encoding: 7bit
42 Content-Description: OpenPGP digital signature
43 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
45 -----BEGIN PGP SIGNATURE-----
47 -----END PGP SIGNATURE-----
51 >>> from email.mime.multipart import MIMEMultipart
52 >>> message = MIMEMultipart()
53 >>> message.attach(encodedMIMEText('Part A'))
54 >>> message.attach(encodedMIMEText('Part B'))
55 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
56 >>> signed.set_boundary('boundsep')
57 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
58 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
60 Content-Disposition: inline
63 Content-Type: multipart/mixed; boundary="===============...=="
66 --===============...==
67 Content-Type: text/plain; charset="us-ascii"
69 Content-Transfer-Encoding: 7bit
70 Content-Disposition: inline
73 --===============...==
74 Content-Type: text/plain; charset="us-ascii"
76 Content-Transfer-Encoding: 7bit
77 Content-Disposition: inline
80 --===============...==--
83 Content-Transfer-Encoding: 7bit
84 Content-Description: OpenPGP digital signature
85 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
87 -----BEGIN PGP SIGNATURE-----
89 -----END PGP SIGNATURE-----
93 body = message.as_string().encode('us-ascii')
94 signature = str(_sign_and_encrypt_bytes(
95 data=body, signers=signers), 'us-ascii')
96 sig = _MIMEApplication(
98 _subtype='pgp-signature; name="signature.asc"',
99 _encoder=_encode_7or8bit)
100 sig['Content-Description'] = 'OpenPGP digital signature'
101 sig.set_charset('us-ascii')
103 msg = _MIMEMultipart(
104 'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
107 msg['Content-Disposition'] = 'inline'
110 def encrypt(message, recipients=None, always_trust=True):
111 r"""Encrypt a ``Message``, returning the encrypted version.
114 +-> application/pgp-encrypted (control information)
115 +-> application/octet-stream (body)
117 >>> from pgp_mime.email import encodedMIMEText
118 >>> message = encodedMIMEText('Hi\nBye')
119 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
120 >>> encrypted = encrypt(message)
121 >>> encrypted.set_boundary('boundsep')
122 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
123 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
125 Content-Disposition: inline
129 Content-Transfer-Encoding: 7bit
130 Content-Type: application/pgp-encrypted; charset="us-ascii"
136 Content-Transfer-Encoding: 7bit
137 Content-Description: OpenPGP encrypted message
138 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
140 -----BEGIN PGP MESSAGE-----
142 -----END PGP MESSAGE-----
146 >>> from email.mime.multipart import MIMEMultipart
147 >>> message = MIMEMultipart()
148 >>> message.attach(encodedMIMEText('Part A'))
149 >>> message.attach(encodedMIMEText('Part B'))
150 >>> encrypted = encrypt(
151 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
152 >>> encrypted.set_boundary('boundsep')
153 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
154 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
156 Content-Disposition: inline
160 Content-Transfer-Encoding: 7bit
161 Content-Type: application/pgp-encrypted; charset="us-ascii"
167 Content-Transfer-Encoding: 7bit
168 Content-Description: OpenPGP encrypted message
169 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
171 -----BEGIN PGP MESSAGE-----
173 -----END PGP MESSAGE-----
177 body = message.as_string().encode('us-ascii')
178 if recipients is None:
179 recipients = [email for name,email in _email_targets(message)]
180 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
181 encrypted = str(_sign_and_encrypt_bytes(
182 data=body, recipients=recipients,
183 always_trust=always_trust), 'us-ascii')
184 enc = _MIMEApplication(
186 _subtype='octet-stream; name="encrypted.asc"',
187 _encoder=_encode_7or8bit)
188 enc['Content-Description'] = 'OpenPGP encrypted message'
189 enc.set_charset('us-ascii')
190 control = _MIMEApplication(
191 _data='Version: 1\n',
192 _subtype='pgp-encrypted',
193 _encoder=_encode_7or8bit)
194 control.set_charset('us-ascii')
195 msg = _MIMEMultipart(
198 protocol='application/pgp-encrypted')
201 msg['Content-Disposition'] = 'inline'
204 def sign_and_encrypt(message, signers=None, recipients=None,
206 r"""Sign and encrypt a ``Message``, returning the encrypted version.
209 +-> application/pgp-encrypted (control information)
210 +-> application/octet-stream (body)
212 >>> from pgp_mime.email import encodedMIMEText
213 >>> message = encodedMIMEText('Hi\nBye')
214 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
215 >>> encrypted = sign_and_encrypt(
216 ... message, signers=['pgp-mime@invalid.com'], always_trust=True)
217 >>> encrypted.set_boundary('boundsep')
218 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
219 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
221 Content-Disposition: inline
225 Content-Transfer-Encoding: 7bit
226 Content-Type: application/pgp-encrypted; charset="us-ascii"
232 Content-Transfer-Encoding: 7bit
233 Content-Description: OpenPGP encrypted message
234 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
236 -----BEGIN PGP MESSAGE-----
238 -----END PGP MESSAGE-----
242 >>> from email.mime.multipart import MIMEMultipart
243 >>> message = MIMEMultipart()
244 >>> message.attach(encodedMIMEText('Part A'))
245 >>> message.attach(encodedMIMEText('Part B'))
246 >>> encrypted = sign_and_encrypt(
247 ... message, signers=['pgp-mime@invalid.com'],
248 ... recipients=['pgp-mime@invalid.com'], always_trust=True)
249 >>> encrypted.set_boundary('boundsep')
250 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
251 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
253 Content-Disposition: inline
257 Content-Transfer-Encoding: 7bit
258 Content-Type: application/pgp-encrypted; charset="us-ascii"
264 Content-Transfer-Encoding: 7bit
265 Content-Description: OpenPGP encrypted message
266 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
268 -----BEGIN PGP MESSAGE-----
270 -----END PGP MESSAGE-----
274 _strip_bcc(message=message)
275 body = message.as_string().encode('us-ascii')
276 if recipients is None:
277 recipients = [email for name,email in _email_targets(message)]
278 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
279 encrypted = str(_sign_and_encrypt_bytes(
280 data=body, signers=signers, recipients=recipients,
281 always_trust=always_trust), 'us-ascii')
282 enc = _MIMEApplication(
284 _subtype='octet-stream; name="encrypted.asc"',
285 _encoder=_encode_7or8bit)
286 enc['Content-Description'] = 'OpenPGP encrypted message'
287 enc.set_charset('us-ascii')
288 control = _MIMEApplication(
289 _data='Version: 1\n',
290 _subtype='pgp-encrypted',
291 _encoder=_encode_7or8bit)
292 control.set_charset('us-ascii')
293 msg = _MIMEMultipart(
296 protocol='application/pgp-encrypted')
299 msg['Content-Disposition'] = 'inline'
302 def _get_encrypted_parts(message):
303 ct = message.get_content_type()
304 assert ct == 'multipart/encrypted', ct
305 params = dict(message.get_params())
306 assert params.get('protocol', None) == 'application/pgp-encrypted', params
307 assert message.is_multipart(), message
308 control = body = None
309 for part in message.get_payload():
312 assert part.is_multipart() == False, part
313 ct = part.get_content_type()
314 if ct == 'application/pgp-encrypted':
316 raise ValueError('multiple application/pgp-encrypted parts')
318 elif ct == 'application/octet-stream':
320 raise ValueError('multiple application/octet-stream parts')
323 raise ValueError('unnecessary {} part'.format(ct))
325 raise ValueError('missing application/pgp-encrypted part')
327 raise ValueError('missing application/octet-stream part')
328 return (control, body)
330 def _get_signed_parts(message):
331 ct = message.get_content_type()
332 assert ct == 'multipart/signed', ct
333 params = dict(message.get_params())
334 assert params.get('protocol', None) == 'application/pgp-signature', params
335 assert message.is_multipart(), message
336 body = signature = None
337 for part in message.get_payload():
340 ct = part.get_content_type()
341 if ct == 'application/pgp-signature':
343 raise ValueError('multiple application/pgp-signature parts')
347 raise ValueError('multiple non-signature parts')
350 raise ValueError('missing body part')
352 raise ValueError('missing application/pgp-signature part')
353 return (body, signature)
355 def decrypt(message):
356 r"""Decrypt a multipart/encrypted message.
358 >>> from pgp_mime.email import encodedMIMEText
359 >>> message = encodedMIMEText('Hi\nBye')
360 >>> encrypted = encrypt(message, recipients=['<pgp-mime@invalid.com>'])
361 >>> decrypted = decrypt(encrypted)
362 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
363 Content-Type: text/plain; charset="us-ascii"
365 Content-Transfer-Encoding: 7bit
366 Content-Disposition: inline
371 >>> from email.mime.multipart import MIMEMultipart
372 >>> message = MIMEMultipart()
373 >>> message.attach(encodedMIMEText('Part A'))
374 >>> message.attach(encodedMIMEText('Part B'))
375 >>> encrypted = encrypt(
376 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
377 >>> decrypted = decrypt(encrypted)
378 >>> decrypted.set_boundary('boundsep')
379 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
380 Content-Type: multipart/mixed; boundary="boundsep"
384 Content-Type: text/plain; charset="us-ascii"
386 Content-Transfer-Encoding: 7bit
387 Content-Disposition: inline
391 Content-Type: text/plain; charset="us-ascii"
393 Content-Transfer-Encoding: 7bit
394 Content-Disposition: inline
400 control,body = _get_encrypted_parts(message)
401 encrypted = body.get_payload(decode=True)
402 if not isinstance(encrypted, bytes):
403 encrypted = encrypted.encode('us-ascii')
404 decrypted,verified,result = _verify_bytes(encrypted)
405 return _message_from_bytes(decrypted)
408 r"""Verify a signature on ``message``, possibly decrypting first.
410 >>> from pgp_mime.email import encodedMIMEText
411 >>> message = encodedMIMEText('Hi\nBye')
412 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
413 >>> encrypted = sign_and_encrypt(message, signers=['pgp-mime@invalid.com'],
414 ... always_trust=True)
415 >>> decrypted,verified,result = verify(encrypted)
416 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
417 Content-Type: text/plain; charset="us-ascii"
419 Content-Transfer-Encoding: 7bit
420 Content-Disposition: inline
421 To: pgp-mime-test <pgp-mime@invalid.com>
427 >>> print(str(result, 'utf-8').replace('\x00', ''))
428 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
429 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
434 <summary value="0x0" />
435 <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
436 <status value="0x0">Success <Unspecified source></status>
437 <timestamp unix="..." />
438 <exp-timestamp unix="0i" />
439 <wrong-key-usage value="0x0" />
440 <pka-trust value="0x0" />
441 <chain-model value="0x0" />
442 <validity value="0x0" />
443 <validity-reason value="0x0">Success <Unspecified source></validity-reason>
444 <pubkey-algo value="0x1">RSA</pubkey-algo>
445 <hash-algo value="0x8">SHA256</hash-algo>
452 >>> from email.mime.multipart import MIMEMultipart
453 >>> message = MIMEMultipart()
454 >>> message.attach(encodedMIMEText('Part A'))
455 >>> message.attach(encodedMIMEText('Part B'))
456 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
457 >>> decrypted,verified,result = verify(signed)
458 >>> decrypted.set_boundary('boundsep')
459 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
460 Content-Type: multipart/mixed; boundary="boundsep"
464 Content-Type: text/plain; charset="us-ascii"
466 Content-Transfer-Encoding: 7bit
467 Content-Disposition: inline
471 Content-Type: text/plain; charset="us-ascii"
473 Content-Transfer-Encoding: 7bit
474 Content-Disposition: inline
480 >>> print(str(result, 'utf-8').replace('\x00', ''))
481 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
482 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
487 <summary value="0x0" />
488 <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
489 <status value="0x0">Success <Unspecified source></status>
490 <timestamp unix="..." />
491 <exp-timestamp unix="0i" />
492 <wrong-key-usage value="0x0" />
493 <pka-trust value="0x0" />
494 <chain-model value="0x0" />
495 <validity value="0x0" />
496 <validity-reason value="0x0">Success <Unspecified source></validity-reason>
497 <pubkey-algo value="0x1">RSA</pubkey-algo>
498 <hash-algo value="0x2">SHA1</hash-algo>
505 ct = message.get_content_type()
506 if ct == 'multipart/encrypted':
507 control,body = _get_encrypted_parts(message)
508 encrypted = body.get_payload(decode=True)
509 if not isinstance(encrypted, bytes):
510 encrypted = encrypted.encode('us-ascii')
511 decrypted,verified,message = _verify_bytes(encrypted)
512 return (_message_from_bytes(decrypted), verified, message)
513 body,signature = _get_signed_parts(message)
514 sig_data = signature.get_payload(decode=True)
515 if not isinstance(sig_data, bytes):
516 sig_data = sig_data.encode('us-ascii')
517 decrypted,verified,result = _verify_bytes(
518 body.as_string().encode('us-ascii'), signature=sig_data)
519 return (body, verified, result)