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, allow_default_signer=False):
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,
96 allow_default_signer=allow_default_signer), 'us-ascii')
97 sig = _MIMEApplication(
99 _subtype='pgp-signature; name="signature.asc"',
100 _encoder=_encode_7or8bit)
101 sig['Content-Description'] = 'OpenPGP digital signature'
102 sig.set_charset('us-ascii')
104 msg = _MIMEMultipart(
105 'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
108 msg['Content-Disposition'] = 'inline'
111 def encrypt(message, recipients=None, always_trust=True):
112 r"""Encrypt a ``Message``, returning the encrypted version.
115 +-> application/pgp-encrypted (control information)
116 +-> application/octet-stream (body)
118 >>> from pgp_mime.email import encodedMIMEText
119 >>> message = encodedMIMEText('Hi\nBye')
120 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
121 >>> encrypted = encrypt(message)
122 >>> encrypted.set_boundary('boundsep')
123 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
124 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
126 Content-Disposition: inline
130 Content-Transfer-Encoding: 7bit
131 Content-Type: application/pgp-encrypted; charset="us-ascii"
137 Content-Transfer-Encoding: 7bit
138 Content-Description: OpenPGP encrypted message
139 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
141 -----BEGIN PGP MESSAGE-----
143 -----END PGP MESSAGE-----
147 >>> from email.mime.multipart import MIMEMultipart
148 >>> message = MIMEMultipart()
149 >>> message.attach(encodedMIMEText('Part A'))
150 >>> message.attach(encodedMIMEText('Part B'))
151 >>> encrypted = encrypt(
152 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
153 >>> encrypted.set_boundary('boundsep')
154 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
155 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
157 Content-Disposition: inline
161 Content-Transfer-Encoding: 7bit
162 Content-Type: application/pgp-encrypted; charset="us-ascii"
168 Content-Transfer-Encoding: 7bit
169 Content-Description: OpenPGP encrypted message
170 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
172 -----BEGIN PGP MESSAGE-----
174 -----END PGP MESSAGE-----
178 body = message.as_string().encode('us-ascii')
179 if recipients is None:
180 recipients = [email for name,email in _email_targets(message)]
181 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
182 encrypted = str(_sign_and_encrypt_bytes(
183 data=body, recipients=recipients,
184 always_trust=always_trust), 'us-ascii')
185 enc = _MIMEApplication(
187 _subtype='octet-stream; name="encrypted.asc"',
188 _encoder=_encode_7or8bit)
189 enc['Content-Description'] = 'OpenPGP encrypted message'
190 enc.set_charset('us-ascii')
191 control = _MIMEApplication(
192 _data='Version: 1\n',
193 _subtype='pgp-encrypted',
194 _encoder=_encode_7or8bit)
195 control.set_charset('us-ascii')
196 msg = _MIMEMultipart(
199 protocol='application/pgp-encrypted')
202 msg['Content-Disposition'] = 'inline'
205 def sign_and_encrypt(message, signers=None, recipients=None,
206 always_trust=False, allow_default_signer=False):
207 r"""Sign and encrypt a ``Message``, returning the encrypted version.
210 +-> application/pgp-encrypted (control information)
211 +-> application/octet-stream (body)
213 >>> from pgp_mime.email import encodedMIMEText
214 >>> message = encodedMIMEText('Hi\nBye')
215 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
216 >>> encrypted = sign_and_encrypt(
217 ... message, signers=['pgp-mime@invalid.com'], always_trust=True)
218 >>> encrypted.set_boundary('boundsep')
219 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
220 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
222 Content-Disposition: inline
226 Content-Transfer-Encoding: 7bit
227 Content-Type: application/pgp-encrypted; charset="us-ascii"
233 Content-Transfer-Encoding: 7bit
234 Content-Description: OpenPGP encrypted message
235 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
237 -----BEGIN PGP MESSAGE-----
239 -----END PGP MESSAGE-----
243 >>> from email.mime.multipart import MIMEMultipart
244 >>> message = MIMEMultipart()
245 >>> message.attach(encodedMIMEText('Part A'))
246 >>> message.attach(encodedMIMEText('Part B'))
247 >>> encrypted = sign_and_encrypt(
248 ... message, signers=['pgp-mime@invalid.com'],
249 ... recipients=['pgp-mime@invalid.com'], always_trust=True)
250 >>> encrypted.set_boundary('boundsep')
251 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
252 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
254 Content-Disposition: inline
258 Content-Transfer-Encoding: 7bit
259 Content-Type: application/pgp-encrypted; charset="us-ascii"
265 Content-Transfer-Encoding: 7bit
266 Content-Description: OpenPGP encrypted message
267 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
269 -----BEGIN PGP MESSAGE-----
271 -----END PGP MESSAGE-----
275 _strip_bcc(message=message)
276 body = message.as_string().encode('us-ascii')
277 if recipients is None:
278 recipients = [email for name,email in _email_targets(message)]
279 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
280 encrypted = str(_sign_and_encrypt_bytes(
281 data=body, signers=signers, recipients=recipients,
282 always_trust=always_trust,
283 allow_default_signer=allow_default_signer), 'us-ascii')
284 enc = _MIMEApplication(
286 _subtype='octet-stream; name="encrypted.asc"',
287 _encoder=_encode_7or8bit)
288 enc['Content-Description'] = 'OpenPGP encrypted message'
289 enc.set_charset('us-ascii')
290 control = _MIMEApplication(
291 _data='Version: 1\n',
292 _subtype='pgp-encrypted',
293 _encoder=_encode_7or8bit)
294 control.set_charset('us-ascii')
295 msg = _MIMEMultipart(
298 protocol='application/pgp-encrypted')
301 msg['Content-Disposition'] = 'inline'
304 def _get_encrypted_parts(message):
305 ct = message.get_content_type()
306 assert ct == 'multipart/encrypted', ct
307 params = dict(message.get_params())
308 assert params.get('protocol', None) == 'application/pgp-encrypted', params
309 assert message.is_multipart(), message
310 control = body = None
311 for part in message.get_payload():
314 assert part.is_multipart() == False, part
315 ct = part.get_content_type()
316 if ct == 'application/pgp-encrypted':
318 raise ValueError('multiple application/pgp-encrypted parts')
320 elif ct == 'application/octet-stream':
322 raise ValueError('multiple application/octet-stream parts')
325 raise ValueError('unnecessary {} part'.format(ct))
327 raise ValueError('missing application/pgp-encrypted part')
329 raise ValueError('missing application/octet-stream part')
330 return (control, body)
332 def _get_signed_parts(message):
333 ct = message.get_content_type()
334 assert ct == 'multipart/signed', ct
335 params = dict(message.get_params())
336 assert params.get('protocol', None) == 'application/pgp-signature', params
337 assert message.is_multipart(), message
338 body = signature = None
339 for part in message.get_payload():
342 ct = part.get_content_type()
343 if ct == 'application/pgp-signature':
345 raise ValueError('multiple application/pgp-signature parts')
349 raise ValueError('multiple non-signature parts')
352 raise ValueError('missing body part')
354 raise ValueError('missing application/pgp-signature part')
355 return (body, signature)
357 def decrypt(message):
358 r"""Decrypt a multipart/encrypted message.
360 >>> from pgp_mime.email import encodedMIMEText
361 >>> message = encodedMIMEText('Hi\nBye')
362 >>> encrypted = encrypt(message, recipients=['<pgp-mime@invalid.com>'])
363 >>> decrypted = decrypt(encrypted)
364 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
365 Content-Type: text/plain; charset="us-ascii"
367 Content-Transfer-Encoding: 7bit
368 Content-Disposition: inline
373 >>> from email.mime.multipart import MIMEMultipart
374 >>> message = MIMEMultipart()
375 >>> message.attach(encodedMIMEText('Part A'))
376 >>> message.attach(encodedMIMEText('Part B'))
377 >>> encrypted = encrypt(
378 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
379 >>> decrypted = decrypt(encrypted)
380 >>> decrypted.set_boundary('boundsep')
381 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
382 Content-Type: multipart/mixed; boundary="boundsep"
386 Content-Type: text/plain; charset="us-ascii"
388 Content-Transfer-Encoding: 7bit
389 Content-Disposition: inline
393 Content-Type: text/plain; charset="us-ascii"
395 Content-Transfer-Encoding: 7bit
396 Content-Disposition: inline
402 control,body = _get_encrypted_parts(message)
403 encrypted = body.get_payload(decode=True)
404 if not isinstance(encrypted, bytes):
405 encrypted = encrypted.encode('us-ascii')
406 decrypted,verified,result = _verify_bytes(encrypted)
407 return _message_from_bytes(decrypted)
410 r"""Verify a signature on ``message``, possibly decrypting first.
412 >>> from pgp_mime.email import encodedMIMEText
413 >>> message = encodedMIMEText('Hi\nBye')
414 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
415 >>> encrypted = sign_and_encrypt(message, signers=['pgp-mime@invalid.com'],
416 ... always_trust=True)
417 >>> decrypted,verified,result = verify(encrypted)
418 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
419 Content-Type: text/plain; charset="us-ascii"
421 Content-Transfer-Encoding: 7bit
422 Content-Disposition: inline
423 To: pgp-mime-test <pgp-mime@invalid.com>
429 >>> print(str(result, 'utf-8').replace('\x00', ''))
430 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
431 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
436 <summary value="0x0" />
437 <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
438 <status value="0x0">Success <Unspecified source></status>
439 <timestamp unix="..." />
440 <exp-timestamp unix="0i" />
441 <wrong-key-usage value="0x0" />
442 <pka-trust value="0x0" />
443 <chain-model value="0x0" />
444 <validity value="0x0" />
445 <validity-reason value="0x0">Success <Unspecified source></validity-reason>
446 <pubkey-algo value="0x1">RSA</pubkey-algo>
447 <hash-algo value="0x8">SHA256</hash-algo>
454 >>> from email.mime.multipart import MIMEMultipart
455 >>> message = MIMEMultipart()
456 >>> message.attach(encodedMIMEText('Part A'))
457 >>> message.attach(encodedMIMEText('Part B'))
458 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
459 >>> decrypted,verified,result = verify(signed)
460 >>> decrypted.set_boundary('boundsep')
461 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
462 Content-Type: multipart/mixed; boundary="boundsep"
466 Content-Type: text/plain; charset="us-ascii"
468 Content-Transfer-Encoding: 7bit
469 Content-Disposition: inline
473 Content-Type: text/plain; charset="us-ascii"
475 Content-Transfer-Encoding: 7bit
476 Content-Disposition: inline
482 >>> print(str(result, 'utf-8').replace('\x00', ''))
483 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
484 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
489 <summary value="0x0" />
490 <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
491 <status value="0x0">Success <Unspecified source></status>
492 <timestamp unix="..." />
493 <exp-timestamp unix="0i" />
494 <wrong-key-usage value="0x0" />
495 <pka-trust value="0x0" />
496 <chain-model value="0x0" />
497 <validity value="0x0" />
498 <validity-reason value="0x0">Success <Unspecified source></validity-reason>
499 <pubkey-algo value="0x1">RSA</pubkey-algo>
500 <hash-algo value="0x2">SHA1</hash-algo>
507 ct = message.get_content_type()
508 if ct == 'multipart/encrypted':
509 control,body = _get_encrypted_parts(message)
510 encrypted = body.get_payload(decode=True)
511 if not isinstance(encrypted, bytes):
512 encrypted = encrypted.encode('us-ascii')
513 decrypted,verified,message = _verify_bytes(encrypted)
514 return (_message_from_bytes(decrypted), verified, message)
515 body,signature = _get_signed_parts(message)
516 sig_data = signature.get_payload(decode=True)
517 if not isinstance(sig_data, bytes):
518 sig_data = sig_data.encode('us-ascii')
519 decrypted,verified,result = _verify_bytes(
520 body.as_string().encode('us-ascii'), signature=sig_data)
521 return (body, verified, result)