Skip to content

Commit 9e95cc3

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 9e95cc3

File tree

5 files changed

+379
-4
lines changed

5 files changed

+379
-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: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
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+
Maintenant qu'on a vu les bases, passons à la pratique.
76+
77+
== L'analyse d'un message chiffré
78+
79+
La structure https://fr.wikipedia.org/wiki/Abstract_Syntax_Notation_One[ASN.1] (Abstract Syntax
80+
Notation One) est un standard de notation utilisé pour représenter des données, couramment utilisé
81+
dans les protocoles de communication et les certificats numériques.
82+
83+
Nous allons étudier un message chiffré, stocké dans le fichier `enveloped.der`. Les messages PKCS#7
84+
sont toujours encodés dans une structure ASN.1. Ici, elle est au format DER, un format binaire
85+
classique pour ce type de structure.
86+
87+
=== Avec OpenSSL
88+
89+
OpenSSL est une bibliothèque logicielle open-source qui fournit des implémentations de nombreux
90+
protocoles de sécurité. Elle est fournie notamment sous forme d'un exécutable en ligne de commande
91+
qui permet de manipuler des certificats, des clés et des messages chiffrés. Cet exécutable est
92+
généralement installé par défaut dans les environnements Conda pour Python, ce qui est très
93+
pratique. Ainsi, on peut l'utiliser tel quel via la bibliothèque built-in `subprocess` :
94+
95+
[source, python]
96+
----
97+
import subprocess
98+
99+
instructions = [
100+
"openssl",
101+
"asn1parse", <1>
102+
"-in",
103+
"vectors/enveloped.der",
104+
"-inform",
105+
"der",
106+
]
107+
output = subprocess.run(instructions, check=True, capture_output=True) <2>
108+
print(output.stdout.decode())
109+
----
110+
<1> La commande `asn1parse` d' `openssl` permet d'analyser la structure ASN.1 du message chiffré.
111+
<2> On utilise `check=True` pour lever une exception si la commande `openssl` échoue.
112+
113+
Résultat :
114+
[source, cmd]
115+
----
116+
0:d=0 hl=4 l= 667 cons: SEQUENCE
117+
4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-envelopedData <1>
118+
15:d=1 hl=4 l= 652 cons: cont [ 0 ]
119+
19:d=2 hl=4 l= 648 cons: SEQUENCE
120+
23:d=3 hl=2 l= 1 prim: INTEGER :00
121+
26:d=3 hl=4 l= 579 cons: SET
122+
30:d=4 hl=4 l= 575 cons: SEQUENCE
123+
34:d=5 hl=2 l= 1 prim: INTEGER :00
124+
37:d=5 hl=2 l= 39 cons: SEQUENCE
125+
39:d=6 hl=2 l= 26 cons: SEQUENCE
126+
41:d=7 hl=2 l= 24 cons: SET
127+
43:d=8 hl=2 l= 22 cons: SEQUENCE
128+
45:d=9 hl=2 l= 3 prim: OBJECT :commonName <2>
129+
50:d=9 hl=2 l= 15 prim: UTF8STRING :cryptography CA
130+
67:d=6 hl=2 l= 9 prim: INTEGER :E712D3A0A56ED6C9
131+
78:d=5 hl=2 l= 13 cons: SEQUENCE
132+
80:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption <3>
133+
91:d=6 hl=2 l= 0 prim: NULL
134+
93:d=5 hl=4 l= 512 prim: OCTET STRING [HEX DUMP]:08086584F1DD436CDA1FB527B243FA02
135+
609:d=3 hl=2 l= 60 cons: SEQUENCE
136+
611:d=4 hl=2 l= 9 prim: OBJECT :pkcs7-data
137+
622:d=4 hl=2 l= 29 cons: SEQUENCE
138+
624:d=5 hl=2 l= 9 prim: OBJECT :aes-128-cbc <4>
139+
635:d=5 hl=2 l= 16 prim: OCTET STRING [HEX DUMP]:2CD7875912507DFC7E65EA7CB86C73BB
140+
653:d=4 hl=2 l= 16 prim: cont [ 0 ]
141+
----
142+
<1> Le type de message est `pkcs7-envelopedData`.
143+
<2> On voit bien un seul certificat, avec un numéro de série `E712D3A0A56ED6C9`.
144+
<3> L'algorithme de chiffrement est `rsaEncryption`, avec la clé chiffrée ci-dessous.
145+
<4> Le contenu est chiffré avec l'algorithme AES, avec une clé de 128
146+
bits et le mode CBC. Son contenu est ci-dessous.
147+
148+
=== Avec `asn1crypto`
149+
150+
On peut faire le même exercice en utilisant la librairie `asn1crypto`. Cette bibliothèque permet de
151+
manipuler des structures ASN.1 de manière plus aisée que `openssl`, en offrant des classes Python
152+
qui peuvent être sérialisées et désérialisées facilement en dictionnaires. Dans notre cas, prenons
153+
l'exemple de la lecture d'une partie de la structure du message chiffré :
154+
155+
[source, python]
156+
----
157+
from asn1crypto import cms
158+
159+
with open("vectors/enveloped.der", "rb") as file:
160+
enveloped = file.read()
161+
162+
# Charger la structure ASN.1
163+
content_info = cms.ContentInfo.load(enveloped) <1>
164+
content_type: cms.ContentType = content_info["content_type"]
165+
enveloped_data: cms.EnvelopedData = content_info["content"]
166+
167+
# Le champ "Encrypted Content Info" contient le contenu chiffré
168+
encrypted_content_info: cms.EncryptedContentInfo = enveloped_data["encrypted_content_info"]
169+
dict(encrypted_content_info.native)
170+
----
171+
<1> Il faut connaître en amont la structure ASN.1 du message pour pouvoir la charger et utiliser le
172+
typage correctement. Cela demande une connaissance préalable des spécifications PKCS #7.
173+
174+
Résultat :
175+
176+
[source, python]
177+
----
178+
{
179+
"content_encryption_algorithm": { <1>
180+
"algorithm": "aes128_cbc",
181+
"parameters": b",\xd7\x87Y\x12P}\xfc~e\xea|\xb8ls\xbb",
182+
},
183+
"content_type": "data",
184+
"encrypted_content": b"[tN\xcb\xdd]\x0b\xa2\xa2\x98T\xf8[t_`",
185+
}
186+
----
187+
<1> On retrouve la même structure que lors de l'analyse avec `openssl`.
188+
189+
== Le déchiffrement
190+
191+
Nous allons maintenant déchiffrer le message chiffré. Pour cela, nous avons besoin des différentes
192+
informations du destinataire, notamment son certificat X.509 et sa clé privée RSA. Ensuite, nous
193+
verrons 3 méthodes pour déchiffrer le message : avec `openssl`, avec `asn1crypto` et avec
194+
`cryptography`.
195+
196+
=== La lecture du certificat et de la clé privée
197+
198+
Nous lisons le certificat X.509 et la clé privée RSA, avec la librairie `cryptography` :
199+
200+
[source, python]
201+
----
202+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
203+
from cryptography.x509 import load_pem_x509_certificate
204+
205+
# Clé publique : certificat RSA
206+
with open("vectors/rsa_ca.pem", "rb") as file:
207+
certificate = load_pem_x509_certificate(file.read())
208+
209+
# Clé privée : RSA
210+
with open("vectors/rsa_key.pem", "rb") as file:
211+
private_key = load_pem_private_key(file.read(), password=None)
212+
----
213+
214+
=== Avec OpenSSL
215+
Nous utilisons `openssl` pour déchiffrer le message :
216+
217+
[source, python]
218+
----
219+
import subprocess
220+
221+
instructions = [
222+
"openssl",
223+
"smime",
224+
"-decrypt", <1>
225+
"-in",
226+
"vectors/enveloped.der",
227+
"-inkey",
228+
"vectors/rsa_key.pem", <2>
229+
"-inform",
230+
"der",
231+
]
232+
output = subprocess.run(instructions, capture_output=True)
233+
output.stdout.decode()
234+
----
235+
<1> La commande `smime -decrypt` permet de déchiffrer un message PKCS #7. Il en existe d'autres
236+
comme `smime -encrypt` ou `cms -decrypt` pour des spécifications PKCS # 7 plus récentes.
237+
<2> Dans cette commande, on ne précise pas le certificat X.509. Il semble que `openssl` tente de
238+
déchiffrer le message avec la clé privée fournie, sans vérifier si elle correspond au certificat
239+
dans le message chiffré.
240+
241+
=== Avec `asn1crypto`
242+
Nous utilisons `asn1crypto` pour analyser et déchiffrer le message :
243+
244+
[source, python]
245+
----
246+
from asn1crypto import cms
247+
from cryptography.hazmat.primitives import padding
248+
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding
249+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
250+
251+
# Charger le message chiffré
252+
with open("vectors/enveloped.der", "rb") as file:
253+
enveloped = file.read()
254+
255+
# Charger la structure ASN.1
256+
content_info = cms.ContentInfo.load(enveloped)
257+
content_type = content_info["content_type"]
258+
enveloped_data = content_info["content"]
259+
260+
# Déchiffrement de la clés de chiffrement symétrique, si possible
261+
decrypted_key = None
262+
for recipient_info in enveloped_data["recipient_infos"].native:
263+
if recipient_info["rid"]["serial_number"] == certificate.serial_number:
264+
decrypted_key = private_key.decrypt(
265+
recipient_info["encrypted_key"], asymmetric_padding.PKCS1v15() <1>
266+
)
267+
break
268+
269+
# On lève une erreur si aucune clé n'est déchiffrable
270+
if decrypted_key is None:
271+
raise ValueError("Aucune clé chiffrée n'est déchiffrable pour le destinataire donné.")
272+
273+
# Déchiffrement du contenu en utilisant la clé symétrique fraîchement déchiffrée
274+
def decrypt(ciphertext, key, initialization_vector) -> str:
275+
cipher = Cipher(algorithms.AES(key), modes.CBC(initialization_vector)) <2>
276+
decryptor = cipher.decryptor()
277+
padded_data = decryptor.update(ciphertext) + decryptor.finalize()
278+
unpadder = padding.PKCS7(len(key) * 8).unpadder() <3>
279+
plaintext = unpadder.update(padded_data) + unpadder.finalize()
280+
return plaintext.decode()
281+
282+
encrypted = enveloped_data["encrypted_content_info"].native
283+
decrypted_content = decrypt(
284+
encrypted["encrypted_content"],
285+
decrypted_key,
286+
encrypted["content_encryption_algorithm"]["parameters"],
287+
)
288+
print("Decrypted content:", decrypted_content)
289+
----
290+
<1> On suppose que le chiffrement de la clé symétrique a été fait avec l'algorithme
291+
`rsaEncryption` et le padding `PKCS1v15`. Cela peut varier selon les implémentations : une autre
292+
possibilité est `RSAES-OAEP` pour un padding plus sécurisé.
293+
<2> Ici, on suppose que l'algorithme de chiffrement du contenu est AES au mode CBC. AES est
294+
l'algorithme le plus répandu, mais d'autres modes sont utilisés dans des nouvelles versions de PKCS
295+
#7.
296+
<3> On utilise la taille de la clé (multipliée par 8, pour passer des octects aux bits) pour
297+
déterminer la taille du padding. Deux tailles sont classiques : 128 et 256 bits.
298+
299+
300+
=== Avec `cryptography`
301+
302+
Enfin, nous utilisons la nouvelle méthode de déchiffrement de messages PKCS #7 intégrée en novembre
303+
2024 à version 44.0.0 de `cryptography` (sortie le 27 novembre 2024) :
304+
305+
[source, python]
306+
----
307+
from cryptography.hazmat.primitives.serialization import pkcs7
308+
309+
with open("vectors/enveloped.der", "rb") as file:
310+
enveloped = file.read()
311+
312+
decrypted = pkcs7.decrypt_der(enveloped, certificate, private_key, []) <1>
313+
print("Decrypted content:", decrypted_content)
314+
----
315+
<1> Le dernier argument est une liste d'options facultatives (voir ci-dessous). Ici, on n'en passe
316+
aucune.
317+
318+
Cette fonctionnalité simple d'utilisation se décline selon les formats des fichiers d'entrée :
319+
320+
- `decrypt_der` pour for format `DER`.
321+
- `decrypt_pem` pour le format `PEM`.
322+
- `decrypt_smime`, pour email sous format texte contenant le message PKCS #7 en pièce jointe.
323+
324+
Elle s'occupe automatiquement de comparer les numéros de série des certificats dans le message
325+
chiffré avec ceux du certificat fourni, gère les deux phases de déchiffrement, et propose des
326+
messages d'erreur compréhensibles en cas de problème (clé privée manquante, algorithme non supporté,
327+
etc.).
328+
329+
Elle possède aussi des options pour nettoyer le texte déchiffré, afin de se rapprocher au maximum du
330+
fonctionnement d'`openssl`. Cela permet un remplacement facile de l'utilisation d'`openssl` dans les
331+
scripts Python.
332+
333+
== Conclusion
334+
Nous avons présenté différentes méthodes pour déchiffrer des emails chiffrés en utilisant Python.
335+
Chacune de ces méthodes ont leurs avantages et leurs inconvénients.
336+
337+
`openssl` est un exécutable facile d'accès contenant toutes les fonctionnalités cryptographiques à
338+
portée de la ligne de commande. Cependant, pour des raisons de sécurité, il est déconseillé
339+
d'utiliser OpenSSL directement, car il a déjà exposé des vulnérabilités par le passé. De plus, c'est
340+
une dépendance supplémentaire, bien qu'elle soit facile à régler via Conda.
341+
342+
La méthode utilisant `asn1crypto` permet de réaliser le déchiffrement 100% en Python, sans
343+
dépendance particulière. Néanmoins, elle demande des connaissances plus avancées en cryptographie et
344+
ne constitue pas une solution clé en main. En effet, la gestion d'autres cas d'usages de PKCS #7
345+
(signature, autres algorithmes) demande de la réflexion. De plus, `asn1crypto` est n'est plus
346+
maintenue depuis fin 2022.
347+
348+
Enfin, la méthode utilisant `cryptography` est la plus simple et la plus sécurisée. Elle est
349+
installée dans une librairie maintenue, et est fiable grâce à l'utilisation de Python & Rust. C'est
350+
la méthode que nous recommandons pour déchiffrer des emails chiffrés en Python.
351+
352+
A noter néanmoins que l'intégration de cette nouvelle fonctionnalité à `cryptography` a pris du
353+
temps à l'équipe SCIAM, entre développement et revues de code. Une contribution open-source prend du
354+
temps, mais c'est un investissement qui en vaut la peine !
355+
356+
== Le Notebook
357+
358+
Vous pouvez retrouver le notebook Jupyter complet de cet article sur notre dépôt GitHub :
359+
https://github.com/SCIAM-FR/email-decryption-demo[SCIAM-FR/email-decryption-demo].
360+
361+
== Les liens utiles
362+
* https://cryptography.io/en/latest/[Cryptography Documentation]
363+
* https://github.com/wbond/asn1crypto[ASN.1 Crypto Documentation]
364+
* 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)