4 from email import message_from_bytes as _message_from_bytes
5 from email.encoders import encode_7or8bit as _encode_7or8bit
6 from email.mime.application import MIMEApplication as _MIMEApplication
7 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
9 from . import LOG as _LOG
10 from .crypt import sign_and_encrypt_bytes as _sign_and_encrypt_bytes
11 from .crypt import verify_bytes as _verify_bytes
12 from .email import email_targets as _email_targets
13 from .email import strip_bcc as _strip_bcc
16 def sign(message, signers=None, allow_default_signer=False):
17 r"""Sign a ``Message``, returning the signed version.
21 +-> application/pgp-signature (signature)
23 >>> from pgp_mime.email import encodedMIMEText
24 >>> message = encodedMIMEText('Hi\nBye')
25 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
26 >>> signed.set_boundary('boundsep')
27 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
28 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
30 Content-Disposition: inline
33 Content-Type: text/plain; charset="us-ascii"
35 Content-Transfer-Encoding: 7bit
36 Content-Disposition: inline
42 Content-Transfer-Encoding: 7bit
43 Content-Description: OpenPGP digital signature
44 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
46 -----BEGIN PGP SIGNATURE-----
48 -----END PGP SIGNATURE-----
52 >>> from email.mime.multipart import MIMEMultipart
53 >>> message = MIMEMultipart()
54 >>> message.attach(encodedMIMEText('Part A'))
55 >>> message.attach(encodedMIMEText('Part B'))
56 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
57 >>> signed.set_boundary('boundsep')
58 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
59 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
61 Content-Disposition: inline
64 Content-Type: multipart/mixed; boundary="===============...=="
67 --===============...==
68 Content-Type: text/plain; charset="us-ascii"
70 Content-Transfer-Encoding: 7bit
71 Content-Disposition: inline
74 --===============...==
75 Content-Type: text/plain; charset="us-ascii"
77 Content-Transfer-Encoding: 7bit
78 Content-Disposition: inline
81 --===============...==--
84 Content-Transfer-Encoding: 7bit
85 Content-Description: OpenPGP digital signature
86 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
88 -----BEGIN PGP SIGNATURE-----
90 -----END PGP SIGNATURE-----
94 body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
95 # use email.policy.SMTP once we get Python 3.3
96 signature = str(_sign_and_encrypt_bytes(
97 data=body, signers=signers,
98 allow_default_signer=allow_default_signer), 'us-ascii')
99 sig = _MIMEApplication(
101 _subtype='pgp-signature; name="signature.asc"',
102 _encoder=_encode_7or8bit)
103 sig['Content-Description'] = 'OpenPGP digital signature'
104 sig.set_charset('us-ascii')
106 msg = _MIMEMultipart(
107 'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
110 msg['Content-Disposition'] = 'inline'
113 def encrypt(message, recipients=None, always_trust=True):
114 r"""Encrypt a ``Message``, returning the encrypted version.
117 +-> application/pgp-encrypted (control information)
118 +-> application/octet-stream (body)
120 >>> from pgp_mime.email import encodedMIMEText
121 >>> message = encodedMIMEText('Hi\nBye')
122 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
123 >>> encrypted = encrypt(message)
124 >>> encrypted.set_boundary('boundsep')
125 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
126 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
128 Content-Disposition: inline
132 Content-Transfer-Encoding: 7bit
133 Content-Type: application/pgp-encrypted; charset="us-ascii"
139 Content-Transfer-Encoding: 7bit
140 Content-Description: OpenPGP encrypted message
141 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
143 -----BEGIN PGP MESSAGE-----
145 -----END PGP MESSAGE-----
149 >>> from email.mime.multipart import MIMEMultipart
150 >>> message = MIMEMultipart()
151 >>> message.attach(encodedMIMEText('Part A'))
152 >>> message.attach(encodedMIMEText('Part B'))
153 >>> encrypted = encrypt(
154 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
155 >>> encrypted.set_boundary('boundsep')
156 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
157 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
159 Content-Disposition: inline
163 Content-Transfer-Encoding: 7bit
164 Content-Type: application/pgp-encrypted; charset="us-ascii"
170 Content-Transfer-Encoding: 7bit
171 Content-Description: OpenPGP encrypted message
172 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
174 -----BEGIN PGP MESSAGE-----
176 -----END PGP MESSAGE-----
180 body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
181 # use email.policy.SMTP once we get Python 3.3
182 if recipients is None:
183 recipients = [email for name,email in _email_targets(message)]
184 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
185 encrypted = str(_sign_and_encrypt_bytes(
186 data=body, recipients=recipients,
187 always_trust=always_trust), 'us-ascii')
188 enc = _MIMEApplication(
190 _subtype='octet-stream; name="encrypted.asc"',
191 _encoder=_encode_7or8bit)
192 enc['Content-Description'] = 'OpenPGP encrypted message'
193 enc.set_charset('us-ascii')
194 control = _MIMEApplication(
195 _data='Version: 1\n',
196 _subtype='pgp-encrypted',
197 _encoder=_encode_7or8bit)
198 control.set_charset('us-ascii')
199 msg = _MIMEMultipart(
202 protocol='application/pgp-encrypted')
205 msg['Content-Disposition'] = 'inline'
208 def sign_and_encrypt(message, signers=None, recipients=None,
209 always_trust=False, allow_default_signer=False):
210 r"""Sign and encrypt a ``Message``, returning the encrypted version.
213 +-> application/pgp-encrypted (control information)
214 +-> application/octet-stream (body)
216 >>> from pgp_mime.email import encodedMIMEText
217 >>> message = encodedMIMEText('Hi\nBye')
218 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
219 >>> encrypted = sign_and_encrypt(
220 ... message, signers=['pgp-mime@invalid.com'], always_trust=True)
221 >>> encrypted.set_boundary('boundsep')
222 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
223 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
225 Content-Disposition: inline
229 Content-Transfer-Encoding: 7bit
230 Content-Type: application/pgp-encrypted; charset="us-ascii"
236 Content-Transfer-Encoding: 7bit
237 Content-Description: OpenPGP encrypted message
238 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
240 -----BEGIN PGP MESSAGE-----
242 -----END PGP MESSAGE-----
246 >>> from email.mime.multipart import MIMEMultipart
247 >>> message = MIMEMultipart()
248 >>> message.attach(encodedMIMEText('Part A'))
249 >>> message.attach(encodedMIMEText('Part B'))
250 >>> encrypted = sign_and_encrypt(
251 ... message, signers=['pgp-mime@invalid.com'],
252 ... recipients=['pgp-mime@invalid.com'], always_trust=True)
253 >>> encrypted.set_boundary('boundsep')
254 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
255 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
257 Content-Disposition: inline
261 Content-Transfer-Encoding: 7bit
262 Content-Type: application/pgp-encrypted; charset="us-ascii"
268 Content-Transfer-Encoding: 7bit
269 Content-Description: OpenPGP encrypted message
270 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
272 -----BEGIN PGP MESSAGE-----
274 -----END PGP MESSAGE-----
278 _strip_bcc(message=message)
279 body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
280 # use email.policy.SMTP once we get Python 3.3
281 if recipients is None:
282 recipients = [email for name,email in _email_targets(message)]
283 _LOG.debug('extracted encryption recipients: {}'.format(recipients))
284 encrypted = str(_sign_and_encrypt_bytes(
285 data=body, signers=signers, recipients=recipients,
286 always_trust=always_trust,
287 allow_default_signer=allow_default_signer), 'us-ascii')
288 enc = _MIMEApplication(
290 _subtype='octet-stream; name="encrypted.asc"',
291 _encoder=_encode_7or8bit)
292 enc['Content-Description'] = 'OpenPGP encrypted message'
293 enc.set_charset('us-ascii')
294 control = _MIMEApplication(
295 _data='Version: 1\n',
296 _subtype='pgp-encrypted',
297 _encoder=_encode_7or8bit)
298 control.set_charset('us-ascii')
299 msg = _MIMEMultipart(
302 protocol='application/pgp-encrypted')
305 msg['Content-Disposition'] = 'inline'
308 def _get_encrypted_parts(message):
309 ct = message.get_content_type()
310 assert ct == 'multipart/encrypted', ct
311 params = dict(message.get_params())
312 assert params.get('protocol', None) == 'application/pgp-encrypted', params
313 assert message.is_multipart(), message
314 control = body = None
315 for part in message.get_payload():
318 assert part.is_multipart() == False, part
319 ct = part.get_content_type()
320 if ct == 'application/pgp-encrypted':
322 raise ValueError('multiple application/pgp-encrypted parts')
324 elif ct == 'application/octet-stream':
326 raise ValueError('multiple application/octet-stream parts')
329 raise ValueError('unnecessary {} part'.format(ct))
331 raise ValueError('missing application/pgp-encrypted part')
333 raise ValueError('missing application/octet-stream part')
334 return (control, body)
336 def _get_signed_parts(message):
337 ct = message.get_content_type()
338 assert ct == 'multipart/signed', ct
339 params = dict(message.get_params())
340 assert params.get('protocol', None) == 'application/pgp-signature', params
341 assert message.is_multipart(), message
342 body = signature = None
343 for part in message.get_payload():
346 ct = part.get_content_type()
347 if ct == 'application/pgp-signature':
349 raise ValueError('multiple application/pgp-signature parts')
353 raise ValueError('multiple non-signature parts')
356 raise ValueError('missing body part')
358 raise ValueError('missing application/pgp-signature part')
359 return (body, signature)
361 def decrypt(message):
362 r"""Decrypt a multipart/encrypted message.
364 >>> from pgp_mime.email import encodedMIMEText
365 >>> message = encodedMIMEText('Hi\nBye')
366 >>> encrypted = encrypt(message, recipients=['<pgp-mime@invalid.com>'])
367 >>> decrypted = decrypt(encrypted)
368 >>> print(decrypted.as_string().replace('\r\n', '\n'))
369 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
370 Content-Type: text/plain; charset="us-ascii"
372 Content-Transfer-Encoding: 7bit
373 Content-Disposition: inline
378 >>> from email.mime.multipart import MIMEMultipart
379 >>> message = MIMEMultipart()
380 >>> message.attach(encodedMIMEText('Part A'))
381 >>> message.attach(encodedMIMEText('Part B'))
382 >>> encrypted = encrypt(
383 ... message, recipients=['pgp-mime@invalid.com'], always_trust=True)
384 >>> decrypted = decrypt(encrypted)
385 >>> decrypted.set_boundary('boundsep')
386 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
387 Content-Type: multipart/mixed; boundary="boundsep"
391 Content-Type: text/plain; charset="us-ascii"
393 Content-Transfer-Encoding: 7bit
394 Content-Disposition: inline
398 Content-Type: text/plain; charset="us-ascii"
400 Content-Transfer-Encoding: 7bit
401 Content-Disposition: inline
407 control,body = _get_encrypted_parts(message)
408 encrypted = body.get_payload(decode=True)
409 if not isinstance(encrypted, bytes):
410 encrypted = encrypted.encode('us-ascii')
411 decrypted,verified,result = _verify_bytes(encrypted)
412 return _message_from_bytes(decrypted)
415 r"""Verify a signature on ``message``, possibly decrypting first.
417 >>> from pgp_mime.email import encodedMIMEText
418 >>> message = encodedMIMEText('Hi\nBye')
419 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
420 >>> encrypted = sign_and_encrypt(message, signers=['pgp-mime@invalid.com'],
421 ... always_trust=True)
422 >>> decrypted,verified,result = verify(encrypted)
423 >>> print(decrypted.as_string().replace('\r\n', '\n'))
424 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
425 Content-Type: text/plain; charset="us-ascii"
427 Content-Transfer-Encoding: 7bit
428 Content-Disposition: inline
429 To: pgp-mime-test <pgp-mime@invalid.com>
435 >>> print(str(result, 'utf-8').replace('\x00', ''))
436 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
437 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
442 <summary value="0x0" />
443 <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
444 <status value="0x0">Success <Unspecified source></status>
445 <timestamp unix="..." />
446 <exp-timestamp unix="0i" />
447 <wrong-key-usage value="0x0" />
448 <pka-trust value="0x0" />
449 <chain-model value="0x0" />
450 <validity value="0x0" />
451 <validity-reason value="0x0">Success <Unspecified source></validity-reason>
452 <pubkey-algo value="0x1">RSA</pubkey-algo>
453 <hash-algo value="0x8">SHA256</hash-algo>
460 >>> from email.mime.multipart import MIMEMultipart
461 >>> message = MIMEMultipart()
462 >>> message.attach(encodedMIMEText('Part A'))
463 >>> message.attach(encodedMIMEText('Part B'))
464 >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
465 >>> decrypted,verified,result = verify(signed)
466 >>> decrypted.set_boundary('boundsep')
467 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
468 Content-Type: multipart/mixed; boundary="boundsep"
472 Content-Type: text/plain; charset="us-ascii"
474 Content-Transfer-Encoding: 7bit
475 Content-Disposition: inline
479 Content-Type: text/plain; charset="us-ascii"
481 Content-Transfer-Encoding: 7bit
482 Content-Disposition: inline
488 >>> print(str(result, 'utf-8').replace('\x00', ''))
489 ... # doctest: +REPORT_UDIFF, +ELLIPSIS
490 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
495 <summary value="0x0" />
496 <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
497 <status value="0x0">Success <Unspecified source></status>
498 <timestamp unix="..." />
499 <exp-timestamp unix="0i" />
500 <wrong-key-usage value="0x0" />
501 <pka-trust value="0x0" />
502 <chain-model value="0x0" />
503 <validity value="0x0" />
504 <validity-reason value="0x0">Success <Unspecified source></validity-reason>
505 <pubkey-algo value="0x1">RSA</pubkey-algo>
506 <hash-algo value="0x2">SHA1</hash-algo>
513 ct = message.get_content_type()
514 if ct == 'multipart/encrypted':
515 control,body = _get_encrypted_parts(message)
516 encrypted = body.get_payload(decode=True)
517 if not isinstance(encrypted, bytes):
518 encrypted = encrypted.encode('us-ascii')
519 decrypted,verified,message = _verify_bytes(encrypted)
520 return (_message_from_bytes(decrypted), verified, message)
521 body,signature = _get_signed_parts(message)
522 sig_data = signature.get_payload(decode=True)
523 if not isinstance(sig_data, bytes):
524 sig_data = sig_data.encode('us-ascii')
525 decrypted,verified,result = _verify_bytes(
526 body.as_string().encode('us-ascii').replace(b'\n', b'\r\n'),
528 # use email.policy.SMTP once we get Python 3.3
529 return (_copy.deepcopy(body), verified, result)