Skip to content

Commit 19f85c9

Browse files
committed
feature: article on email decryption
feature: added bio for Quentin Retourne fix/settings: new name antora support parameter taken Jean-Michel's feedback into account added some verbatim to explain the code
1 parent 4cbef5f commit 19f85c9

File tree

5 files changed

+377
-4
lines changed

5 files changed

+377
-4
lines changed

.vscode/settings.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"asciidoc.extensions.enableKroki": true,
33
"asciidoc.preview.asciidoctorAttributes": {
4-
"imagesdir":"../images",
5-
"source-highlighter":"highlightjs",
6-
"stem":"latexmath"
4+
"imagesdir": "../images",
5+
"source-highlighter": "highlightjs",
6+
"stem": "latexmath"
77
},
88
"cSpell.language": "en,fr,fr-FR",
9-
"asciidoc.antora.enableAntoraSupport": false
9+
"asciidoc.antora.showEnableAntoraPrompt": true
1010
}

_data/authors.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,17 @@ christophesejourne:
193193
socials:
194194
linkedin: "christophesejourne"
195195

196+
197+
quentinretourne:
198+
name: "Quentin Retourné"
199+
bio: "Quentin Retourné a rejoint SCIAM en mars 2018 avec une envie : utiliser la Data Science pour résoudre des problèmes complexes le plus simplement possible. Il a suivi des études de Data Science et Génie Industriel à CentraleSupélec et Columbia University à New York. Quentin a grandi à l'étranger pendant 11 ans, ce qui l'a baigné dans la diversité et lui a donné une grande faculté d'adaptation. Féru de sport, il a pratiqué la natation en compétition, et s'adonne maintenant à l'escalade et au volleyball. C'est aussi un grand fan d'énigmes !"
200+
201+
job: "Consultant Sénior Data/IA"
202+
pagesciam: "https://www.sciam.fr/equipe/quentin-retourne"
203+
picture: quentinretourne.jpg
204+
socials:
205+
linkedin: "quentinretourne"
206+
196207
pierrelepagnol:
197208
name: "Pierre Lepagnol"
198209
bio: "PhD Student in CS @LISN/Paris-Saclay University & Data Scientist"
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
= Déchiffrement d'Emails avec Python
2+
:showtitle:
3+
:page-navtitle: Déchiffrement d'Emails avec Python
4+
:page-excerpt: Découvrez comment déchiffrer des emails chiffrés avec Python en utilisant `openssl`, `asn1crypto` et `cryptography`.
5+
:layout: post
6+
:author: quentinretourne
7+
:page-tags: [Tutoriel, Python, Cryptographie]
8+
:page-vignette: email-decryption.png
9+
:page-categories: software
10+
11+
Le déchiffrement d'emails chiffrés est une tâche essentielle pour garantir la confidentialité des
12+
communications. Dans cet article, nous présentons différentes solutions pour déchiffrer des emails
13+
chiffrés en utilisant Python, en particulier avec l'exécutable `openssl` et les bibliothèques
14+
`asn1crypto` et `cryptography`. Nous terminons en présentant une nouvelle méthode de déchiffrement
15+
de mails chiffrés, développée par nos soins, qui vient d'être intégrée à `cryptography` en novembre
16+
2024.
17+
18+
== Quelques prérequis
19+
20+
Avant de se plonger dans le déchiffrement des emails, il est important de comprendre quelques concepts
21+
de base en cryptographie.
22+
23+
=== La cryptographie et le chiffrement
24+
25+
La cryptographie est l'art de sécuriser les communications en les transformant afin qu'elles ne
26+
puissent être lues que par les destinataires prévus. Un message chiffré n'aura pas de sens pour un
27+
observateur externe. En cryptographie, il s'agit donc de chiffrer et de déchiffrer des messages.
28+
Pour ce faire, plusieurs algorithmes de chiffrement sont utilisés ; nous allons parcourir ceux qui
29+
nous seront utiles dans cet article.
30+
31+
Le https://fr.wikipedia.org/wiki/Chiffrement_RSA[chiffrement RSA] est un algorithme de cryptographie
32+
asymétrique qui utilise une paire de clés : une clé publique pour le chiffrement et une clé privée
33+
pour le déchiffrement. RSA est largement utilisé pour sécuriser les communications sur Internet,
34+
notamment pour les certificats SSL/TLS et les connexions SSH.
35+
36+
Par opposition au chiffrement asymétrique (dont RSA fait partie), on peut aussi chiffrer des
37+
messages de manière symétrique. Dans ce cas, on utilise la même clé pour chiffrer et déchiffrer les
38+
messages. Cela est utile pour d'autres cas d'usages : le chiffrement symétrique est généralement
39+
plus rapide que le chiffrement asymétrique (même si cela dépend des algorithmes utilisés).
40+
41+
Ces deux types de chiffrement sont utilisés dans le format PKCS #7 pour chiffrer des emails, que
42+
nous allons étudier ci-dessous.
43+
44+
=== Le format PKCS #7
45+
46+
Les spécifications https://en.wikipedia.org/wiki/PKCS_7[PKCS #7] (Public Key Cryptography Standards
47+
#7) définissent un format standard pour sécuriser des messages, et notamment des emails. PKCS #7 est
48+
un format complet, qui permet notamment de signer / vérifier des messages ainsi que les chiffrer /
49+
déchiffrer. Dans cet article, nous allons nous concentrer sur les spécifications de "chiffrement /
50+
déchiffrement" de PKCS #7 par opposition à la partie "signature". On les désigne aussi par les
51+
termes "encapsulation" ou "enveloped data" en anglais.
52+
53+
Dans le cas de chiffrement d'un message PKCS #7, deux chiffrements ont lieu:
54+
55+
1. Le chiffrement du contenu du message, selon un algorithme de chiffrement symétrique nécessitant
56+
une clé. Cette clé est générée de manière aléatoire.
57+
2. Le(s) chiffrement(s) de la clé ci-dessus, cette fois-ci de façon asymétrique, pour chaque destinataire
58+
du message. Cela est fait avec la/les clé(s) RSA publique(s) du ou des destinataires, qui doivent
59+
être mises à disposition de l'expéditeur du message.
60+
61+
Ainsi, lors du déchiffrement, chaque destinataire va :
62+
63+
- Inspecter chaque clé chiffrée dans le message sous format PKCS #7, et trouver celle qui
64+
correspond à sa clé RSA publique.
65+
- Déchiffrer la clé symétrique avec sa clé RSA privée.
66+
- Déchiffrer le contenu du message avec la clé symétrique fraîchement déchiffrée.
67+
68+
L'avantage de cette méthode permet d'économiser du temps de calcul et de réduire la taille du
69+
message à transporter. En effet, le contenu (qui n'a pas de limite de taille) est chiffré une seule
70+
fois avec un algorithme symétrique, beaucoup plus rapide que ses homologues asymétriques. De plus,
71+
c'est uniquement la clé symétrique (entre 128 et 256 bits selon les algorithmes) qui est chiffrée
72+
plusieurs fois pour chaque destinataire par un algorithme asymétrique et stockée dans la structure
73+
du message.
74+
75+
== L'analyse d'un message chiffré
76+
77+
La structure https://fr.wikipedia.org/wiki/Abstract_Syntax_Notation_One[ASN.1] (Abstract Syntax
78+
Notation One) est un standard de notation utilisé pour représenter des données, couramment utilisé
79+
dans les protocoles de communication et les certificats numériques.
80+
81+
Nous allons étudier un message chiffré, stocké dans le fichier `enveloped.der`. Les messages PKCS#7
82+
sont toujours encodés dans une structure ASN.1. Ici, elle est au format DER, un format binaire
83+
classique pour ce type de structure.
84+
85+
=== Avec OpenSSL
86+
87+
OpenSSL est une bibliothèque logicielle open-source qui fournit des implémentations de nombreux
88+
protocoles de sécurité. Elle est fournie notamment sous forme d'un exécutable en ligne de commande
89+
qui permet de manipuler des certificats, des clés et des messages chiffrés. Cet exécutable est
90+
généralement installé par défaut dans les environnements Conda pour Python, ce qui est très
91+
pratique. Ainsi, on peut l'utiliser tel quel via la bibliothèque built-in `subprocess` :
92+
93+
[source, python]
94+
----
95+
import subprocess
96+
97+
instructions = [
98+
"openssl",
99+
"asn1parse", <1>
100+
"-in",
101+
"vectors/enveloped.der",
102+
"-inform",
103+
"der",
104+
]
105+
output = subprocess.run(instructions, check=True, capture_output=True) <2>
106+
print(output.stdout.decode())
107+
----
108+
<1> La commande `asn1parse` d' `openssl` permet d'analyser la structure ASN.1 du message chiffré.
109+
<2> On utilise `check=True` pour lever une exception si la commande `openssl` échoue.
110+
111+
Résultat :
112+
[source, cmd]
113+
----
114+
0:d=0 hl=4 l= 667 cons: SEQUENCE
115+
4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-envelopedData <1>
116+
15:d=1 hl=4 l= 652 cons: cont [ 0 ]
117+
19:d=2 hl=4 l= 648 cons: SEQUENCE
118+
23:d=3 hl=2 l= 1 prim: INTEGER :00
119+
26:d=3 hl=4 l= 579 cons: SET
120+
30:d=4 hl=4 l= 575 cons: SEQUENCE
121+
34:d=5 hl=2 l= 1 prim: INTEGER :00
122+
37:d=5 hl=2 l= 39 cons: SEQUENCE
123+
39:d=6 hl=2 l= 26 cons: SEQUENCE
124+
41:d=7 hl=2 l= 24 cons: SET
125+
43:d=8 hl=2 l= 22 cons: SEQUENCE
126+
45:d=9 hl=2 l= 3 prim: OBJECT :commonName <2>
127+
50:d=9 hl=2 l= 15 prim: UTF8STRING :cryptography CA
128+
67:d=6 hl=2 l= 9 prim: INTEGER :E712D3A0A56ED6C9
129+
78:d=5 hl=2 l= 13 cons: SEQUENCE
130+
80:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption <3>
131+
91:d=6 hl=2 l= 0 prim: NULL
132+
93:d=5 hl=4 l= 512 prim: OCTET STRING [HEX DUMP]:08086584F1DD436CDA1FB527B243FA02
133+
609:d=3 hl=2 l= 60 cons: SEQUENCE
134+
611:d=4 hl=2 l= 9 prim: OBJECT :pkcs7-data
135+
622:d=4 hl=2 l= 29 cons: SEQUENCE
136+
624:d=5 hl=2 l= 9 prim: OBJECT :aes-128-cbc <4>
137+
635:d=5 hl=2 l= 16 prim: OCTET STRING [HEX DUMP]:2CD7875912507DFC7E65EA7CB86C73BB
138+
653:d=4 hl=2 l= 16 prim: cont [ 0 ]
139+
----
140+
<1> Le type de message est `pkcs7-envelopedData`.
141+
<2> On voit bien un seul certificat, avec un numéro de série `E712D3A0A56ED6C9`.
142+
<3> L'algorithme de chiffrement est `rsaEncryption`, avec la clé chiffrée ci-dessous.
143+
<4> Le contenu est chiffré avec l'algorithme AES, avec une clé de 128
144+
bits et le mode CBC. Son contenu est ci-dessous.
145+
146+
=== Avec `asn1crypto`
147+
148+
On peut faire le même exercice en utilisant la librairie `asn1crypto`. Cette bibliothèque permet de
149+
manipuler des structures ASN.1 de manière plus aisée que `openssl`, en offrant des classes Python
150+
qui peuvent être sérialisées et désérialisées facilement en dictionnaires. Dans notre cas, prenons
151+
l'exemple de la lecture d'une partie de la structure du message chiffré :
152+
153+
[source, python]
154+
----
155+
from asn1crypto import cms
156+
157+
with open("vectors/enveloped.der", "rb") as file:
158+
enveloped = file.read()
159+
160+
# Charger la structure ASN.1
161+
content_info = cms.ContentInfo.load(enveloped) <1>
162+
content_type: cms.ContentType = content_info["content_type"]
163+
enveloped_data: cms.EnvelopedData = content_info["content"]
164+
165+
# Le champ "Encrypted Content Info" contient le contenu chiffré
166+
encrypted_content_info: cms.EncryptedContentInfo = enveloped_data["encrypted_content_info"]
167+
dict(encrypted_content_info.native)
168+
----
169+
<1> Il faut connaître en amont la structure ASN.1 du message pour pouvoir la charger et utiliser le
170+
typage correctement. Cela demande une connaissance préalable des spécifications PKCS #7.
171+
172+
Résultat :
173+
174+
[source, python]
175+
----
176+
{
177+
"content_encryption_algorithm": { <1>
178+
"algorithm": "aes128_cbc",
179+
"parameters": b",\xd7\x87Y\x12P}\xfc~e\xea|\xb8ls\xbb",
180+
},
181+
"content_type": "data",
182+
"encrypted_content": b"[tN\xcb\xdd]\x0b\xa2\xa2\x98T\xf8[t_`",
183+
}
184+
----
185+
<1> On retrouve la même structure que lors de l'analyse avec `openssl`.
186+
187+
== Le déchiffrement
188+
189+
Nous allons maintenant déchiffrer le message chiffré. Pour cela, nous avons besoin des différentes
190+
informations du destinataire, notamment son certificat X.509 et sa clé privée RSA. Ensuite, nous
191+
verrons 3 méthodes pour déchiffrer le message : avec `openssl`, avec `asn1crypto` et avec
192+
`cryptography`.
193+
194+
=== La lecture du certificat et de la clé privée
195+
196+
Nous lisons le certificat X.509 et la clé privée RSA, avec la librairie `cryptography` :
197+
198+
[source, python]
199+
----
200+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
201+
from cryptography.x509 import load_pem_x509_certificate
202+
203+
# Clé publique : certificat RSA
204+
with open("vectors/rsa_ca.pem", "rb") as file:
205+
certificate = load_pem_x509_certificate(file.read())
206+
207+
# Clé privée : RSA
208+
with open("vectors/rsa_key.pem", "rb") as file:
209+
private_key = load_pem_private_key(file.read(), password=None)
210+
----
211+
212+
=== Avec OpenSSL
213+
Nous utilisons `openssl` pour déchiffrer le message :
214+
215+
[source, python]
216+
----
217+
import subprocess
218+
219+
instructions = [
220+
"openssl",
221+
"smime",
222+
"-decrypt", <1>
223+
"-in",
224+
"vectors/enveloped.der",
225+
"-inkey",
226+
"vectors/rsa_key.pem", <2>
227+
"-inform",
228+
"der",
229+
]
230+
output = subprocess.run(instructions, capture_output=True)
231+
output.stdout.decode()
232+
----
233+
<1> La commande `smime -decrypt` permet de déchiffrer un message PKCS #7. Il en existe d'autres
234+
comme `smime -encrypt` ou `cms -decrypt` pour des spécifications PKCS # 7 plus récentes.
235+
<2> Dans cette commande, on ne précise pas le certificat X.509. Il semble qu' `openssl` tente de
236+
déchiffrer le message avec la clé privée fournie, sans vérifier si elle correspond au certificat
237+
dans le message chiffré.
238+
239+
=== Avec `asn1crypto`
240+
Nous utilisons `asn1crypto` pour analyser et déchiffrer le message :
241+
242+
[source, python]
243+
----
244+
from asn1crypto import cms
245+
from cryptography.hazmat.primitives import padding
246+
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding
247+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
248+
249+
# Charger le message chiffré
250+
with open("vectors/enveloped.der", "rb") as file:
251+
enveloped = file.read()
252+
253+
# Charger la structure ASN.1
254+
content_info = cms.ContentInfo.load(enveloped)
255+
content_type = content_info["content_type"]
256+
enveloped_data = content_info["content"]
257+
258+
# Déchiffrement de la clés de chiffrement symétrique, si possible
259+
decrypted_key = None
260+
for recipient_info in enveloped_data["recipient_infos"].native:
261+
if recipient_info["rid"]["serial_number"] == certificate.serial_number:
262+
decrypted_key = private_key.decrypt(
263+
recipient_info["encrypted_key"], asymmetric_padding.PKCS1v15() <1>
264+
)
265+
break
266+
267+
# On lève une erreur si aucune clé n'est déchiffrable
268+
if decrypted_key is None:
269+
raise ValueError("Aucune clé chiffrée n'est déchiffrable pour le destinataire donné.")
270+
271+
# Déchiffrement du contenu en utilisant la clé symétrique fraîchement déchiffrée
272+
def decrypt(ciphertext, key, initialization_vector) -> str:
273+
cipher = Cipher(algorithms.AES(key), modes.CBC(initialization_vector)) <2>
274+
decryptor = cipher.decryptor()
275+
padded_data = decryptor.update(ciphertext) + decryptor.finalize()
276+
unpadder = padding.PKCS7(len(key) * 8).unpadder() <3>
277+
plaintext = unpadder.update(padded_data) + unpadder.finalize()
278+
return plaintext.decode()
279+
280+
encrypted = enveloped_data["encrypted_content_info"].native
281+
decrypted_content = decrypt(
282+
encrypted["encrypted_content"],
283+
decrypted_key,
284+
encrypted["content_encryption_algorithm"]["parameters"],
285+
)
286+
print("Decrypted content:", decrypted_content)
287+
----
288+
<1> On suppose ici que le chiffrement de la clé symétrique a été fait avec l'algorithme
289+
`rsaEncryption` et le padding `PKCS1v15`. Cela peut varier selon les implémentations : une autre
290+
possibilité est `RSAES-OAEP` pour un padding plus sécurisé.
291+
<2> On suppose ici que l'algorithme de chiffrement du contenu est AES au mode CBC. AES est
292+
l'algorithme le plus répandu, mais d'autres modes que CBC, comme GCM, sont utilisés dans des cas
293+
plus récents.
294+
<3> On utilise la taille de la clé (multipliée par 8, pour passer des octects aux bits) pour
295+
déterminer la taille du padding. Deux tailles sont classiques : 128 et 256 bits.
296+
297+
298+
=== Avec `cryptography`
299+
300+
Enfin, nous utilisons la nouvelle méthode de déchiffrement de messages PKCS #7 de la version 44.0.0
301+
de `cryptography`, sortie le 27 novembre 2024 :
302+
303+
[source, python]
304+
----
305+
from cryptography.hazmat.primitives.serialization import pkcs7
306+
307+
with open("vectors/enveloped.der", "rb") as file:
308+
enveloped = file.read()
309+
310+
decrypted = pkcs7.decrypt_der(enveloped, certificate, private_key, []) <1>
311+
print("Decrypted content:", decrypted_content)
312+
----
313+
<1> Le dernier argument est une liste d'options facultatives (voir ci-dessous). Ici, on n'en passe
314+
aucune.
315+
316+
Cette fonctionnalité simple d'utilisation se décline selon les formats des fichiers d'entrée :
317+
318+
- `decrypt_der` pour for format `DER`.
319+
- `decrypt_pem` pour le format `PEM`.
320+
- `decrypt_smime` pour email sous format texte contenant le message PKCS #7 en pièce jointe.
321+
322+
Elle s'occupe automatiquement de comparer les numéros de série des certificats dans le message
323+
chiffré avec ceux du certificat fourni, gère les deux phases de déchiffrement, et propose des
324+
messages d'erreur compréhensibles en cas de problème (clé privée manquante, algorithme non supporté,
325+
etc.).
326+
327+
Elle possède aussi des options pour nettoyer le texte déchiffré, afin de se rapprocher au maximum du
328+
fonctionnement d' `openssl`. Cela permet un remplacement facile de l'utilisation d' `openssl` dans
329+
les scripts Python.
330+
331+
== Conclusion
332+
Nous avons présenté différentes méthodes pour déchiffrer des emails chiffrés en utilisant Python.
333+
Chacune de ces méthodes ont leurs avantages et leurs inconvénients.
334+
335+
`openssl` est un exécutable facile d'accès contenant toutes les fonctionnalités cryptographiques à
336+
portée de la ligne de commande. Cependant, pour des raisons de sécurité, il est déconseillé
337+
d'utiliser OpenSSL directement, car il a déjà exposé des vulnérabilités par le passé. De plus, c'est
338+
une dépendance supplémentaire, bien qu'elle soit facile à régler via Conda.
339+
340+
La méthode utilisant `asn1crypto` permet de réaliser le déchiffrement 100% en Python, sans
341+
dépendance particulière. Néanmoins, elle demande des connaissances plus avancées en cryptographie et
342+
ne constitue pas une solution clé en main. En effet, la gestion d'autres cas d'usages de PKCS #7
343+
(signature, autres algorithmes) demande de la réflexion. De plus, `asn1crypto` est n'est plus
344+
maintenue depuis fin 2022.
345+
346+
Enfin, la méthode utilisant `cryptography` est la plus simple et la plus sécurisée. Elle est
347+
installée dans une librairie maintenue, et est fiable grâce à l'utilisation de Python & Rust. C'est
348+
la méthode que nous recommandons pour déchiffrer des emails chiffrés en Python.
349+
350+
A noter néanmoins que l'intégration de cette nouvelle fonctionnalité à `cryptography` a pris du
351+
temps à l'équipe SCIAM, entre développement et revues de code. Une contribution open-source prend du
352+
temps, mais c'est un investissement qui en vaut la peine !
353+
354+
== Le notebook
355+
356+
Vous pouvez retrouver le notebook Jupyter complet de cet article sur notre dépôt GitHub :
357+
https://github.com/SCIAM-FR/email-decryption-demo[SCIAM-FR/email-decryption-demo].
358+
359+
== Les liens utiles
360+
* https://cryptography.io/en/latest/[Cryptography Documentation]
361+
* https://github.com/wbond/asn1crypto[ASN.1 Crypto Documentation]
362+
* https://www.baeldung.com/cs/public-key-cryptography-standards[Article sur PKCS #7]

images/authors/quentinretourne.jpg

60.6 KB
Loading

images/vignettes/email-decryption.png

472 KB
Loading

0 commit comments

Comments
 (0)