Skip to content

Commit 7817566

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
1 parent 4cbef5f commit 7817566

File tree

5 files changed

+359
-4
lines changed

5 files changed

+359
-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: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
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",
102+
"-in",
103+
"vectors/enveloped.der",
104+
"-inform",
105+
"der",
106+
]
107+
output = subprocess.run(instructions, check=True, capture_output=True)
108+
print(output.stdout.decode())
109+
----
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
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
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
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
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+
141+
Dans cette structure, on observe bien les différents morceaux identifiés précédemment, comme :
142+
143+
- Le type de message (pkcs7-envelopedData).
144+
- Les clés chiffrées et l'algorithme utilisé (RSA) pour chaque destinataire (ici, il n'y en a qu'un).
145+
- Le contenu chiffré et l'algorithme utilisé (AES 128 CBC)
146+
147+
=== Avec `asn1crypto`
148+
149+
On peut faire le même exercice en utilisant la librairie `asn1crypto`. Cette bibliothèque permet de
150+
manipuler des structures ASN.1 de manière plus aisée que `openssl`, en offrant des classes Python
151+
qui peuvent être sérialisées et désérialisées facilement en dictionnaires. Dans notre cas, prenons
152+
l'exemple de la lecture d'une partie de la structure du message chiffré :
153+
154+
[source, python]
155+
----
156+
from asn1crypto import cms
157+
158+
with open("vectors/enveloped.der", "rb") as file:
159+
enveloped = file.read()
160+
161+
# Charger la structure ASN.1
162+
content_info = cms.ContentInfo.load(enveloped)
163+
content_type: cms.ContentType = content_info["content_type"]
164+
enveloped_data: cms.EnvelopedData = content_info["content"]
165+
166+
# Le champ "Encrypted Content Info" contient le contenu chiffré
167+
encrypted_content_info: cms.EncryptedContentInfo = enveloped_data["encrypted_content_info"]
168+
dict(encrypted_content_info.native)
169+
----
170+
171+
Résultat :
172+
173+
[source, python]
174+
----
175+
{
176+
"content_encryption_algorithm": {
177+
"algorithm": "aes128_cbc",
178+
"parameters": b",\xd7\x87Y\x12P}\xfc~e\xea|\xb8ls\xbb",
179+
},
180+
"content_type": "data",
181+
"encrypted_content": b"[tN\xcb\xdd]\x0b\xa2\xa2\x98T\xf8[t_`",
182+
}
183+
----
184+
185+
== Le déchiffrement
186+
187+
Nous allons maintenant déchiffrer le message chiffré. Pour cela, nous avons besoin des différentes
188+
informations du destinataire, notamment son certificat X.509 et sa clé privée RSA. Ensuite, nous
189+
verrons 3 méthodes pour déchiffrer le message : avec `openssl`, avec `asn1crypto` et avec
190+
`cryptography`.
191+
192+
=== La lecture du certificat et de la clé privée
193+
194+
Nous lisons le certificat X.509 et la clé privée RSA, avec la librairie `cryptography` :
195+
196+
[source, python]
197+
----
198+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
199+
from cryptography.x509 import load_pem_x509_certificate
200+
201+
# Clé publique : certificat RSA
202+
with open("vectors/rsa_ca.pem", "rb") as file:
203+
certificate = load_pem_x509_certificate(file.read())
204+
205+
# Clé privée : RSA
206+
with open("vectors/rsa_key.pem", "rb") as file:
207+
private_key = load_pem_private_key(file.read(), password=None)
208+
----
209+
210+
=== Avec OpenSSL
211+
Nous utilisons `openssl` pour déchiffrer le message :
212+
213+
[source, python]
214+
----
215+
import subprocess
216+
217+
instructions = [
218+
"openssl",
219+
"cms",
220+
"-decrypt",
221+
"-in",
222+
"vectors/enveloped.der",
223+
"-inkey",
224+
"vectors/rsa_key.pem",
225+
"-inform",
226+
"der",
227+
]
228+
output = subprocess.run(instructions, capture_output=True)
229+
output.stdout.decode()
230+
----
231+
232+
=== Avec `asn1crypto`
233+
Nous utilisons `asn1crypto` pour analyser et déchiffrer le message :
234+
235+
[source, python]
236+
----
237+
from asn1crypto import cms
238+
from cryptography.hazmat.primitives import padding
239+
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding
240+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
241+
242+
# Charger le message chiffré
243+
with open("vectors/enveloped.der", "rb") as file:
244+
enveloped = file.read()
245+
246+
# On lie les clés privées avec les numéros de série des certificats
247+
# Cela permet au destinataire de retrouver sa clé privée
248+
private_keys = {certificate.serial_number: private_key}
249+
250+
# Charger la structure ASN.1
251+
content_info = cms.ContentInfo.load(enveloped)
252+
content_type = content_info["content_type"]
253+
enveloped_data = content_info["content"]
254+
255+
# Déchiffrer les clés de chiffrement pour chaque destinataire, si possible
256+
decrypted_keys = {}
257+
for recipient_info in enveloped_data["recipient_infos"].native:
258+
serial_number = recipient_info["rid"]["serial_number"]
259+
if serial_number in private_keys:
260+
decrypted_keys[serial_number] = private_keys[serial_number].decrypt(
261+
recipient_info["encrypted_key"], asymmetric_padding.PKCS1v15()
262+
)
263+
264+
# Déchiffrer le contenu via l'algorithme AES-128-CBC
265+
def decrypt(ciphertext, key, initialization_vector) -> str:
266+
cipher = Cipher(algorithms.AES(key), modes.CBC(initialization_vector))
267+
decryptor = cipher.decryptor()
268+
padded_data = decryptor.update(ciphertext) + decryptor.finalize()
269+
unpadder = padding.PKCS7(algorithms.AES128.block_size).unpadder()
270+
plaintext = unpadder.update(padded_data) + unpadder.finalize()
271+
return plaintext.decode()
272+
273+
encrypted = enveloped_data["encrypted_content_info"].native
274+
decrypted_content = decrypt(
275+
encrypted["encrypted_content"],
276+
decrypted_keys[certificate.serial_number],
277+
encrypted["content_encryption_algorithm"]["parameters"],
278+
)
279+
print("Decrypted content:", decrypted_content)
280+
----
281+
282+
=== Avec `cryptography`
283+
284+
Enfin, nous utilisons la nouvelle méthode de déchiffrement de messages PKCS #7 intégrée en novembre
285+
2024 à version 44.0.0 de `cryptography` (sortie le 27 novembre 2024) :
286+
287+
[source, python]
288+
----
289+
from cryptography.hazmat.primitives.serialization import pkcs7
290+
291+
with open("vectors/enveloped.der", "rb") as file:
292+
enveloped = file.read()
293+
294+
decrypted = pkcs7.decrypt_der(enveloped, certificate, private_key, [])
295+
print("Decrypted content:", decrypted_content)
296+
----
297+
298+
Cette fonctionnalité simple d'utilisation se décline selon les formats des fichiers d'entrée :
299+
300+
- `decrypt_der` pour for format `DER`.
301+
- `decrypt_pem` pour le format `PEM`.
302+
- `decrypt_smime`, pour email sous format texte contenant le message PKCS #7 en pièce jointe.
303+
304+
Elle s'occupe automatiquement de comparer les numéros de série des certificats dans le message
305+
chiffré avec ceux du certificat fourni, gère les deux phases de déchiffrement, et propose des
306+
messages d'erreur compréhensibles en cas de problème (clé privée manquante, algorithme non supporté,
307+
etc.).
308+
309+
Elle possède aussi des options pour nettoyer le texte déchiffré, afin de se rapprocher au maximum du
310+
fonctionnement d'`openssl`. Cela permet un remplacement facile de l'utilisation d'`openssl` dans les
311+
scripts Python.
312+
313+
== Conclusion
314+
Nous avons présenté différentes méthodes pour déchiffrer des emails chiffrés en utilisant Python.
315+
Chacune de ces méthodes ont leurs avantages et leurs inconvénients.
316+
317+
`openssl` est un exécutable facile d'accès contenant toutes les fonctionnalités cryptographiques à
318+
portée de la ligne de commande. Cependant, pour des raisons de sécurité, il est déconseillé
319+
d'utiliser OpenSSL directement, car il a déjà exposé des vulnérabilités par le passé. De plus, c'est
320+
une dépendance supplémentaire, bien qu'elle soit facile à régler via Conda.
321+
322+
La méthode utilisant `asn1crypto` permet de réaliser le déchiffrement 100% en Python, sans
323+
dépendance particulière. Néanmoins, elle demande des connaissances plus avancées en cryptographie et
324+
ne constitue pas une solution clé en main. En effet, la gestion d'autres cas d'usages de PKCS #7
325+
(signature, autres algorithmes) demande de la réflexion. De plus, `asn1crypto` est n'est plus
326+
maintenue depuis fin 2022.
327+
328+
Enfin, la méthode utilisant `cryptography` est la plus simple et la plus sécurisée. Elle est
329+
installée dans une librairie maintenue, et est fiable grâce à l'utilisation de Python & Rust. C'est
330+
la méthode que nous recommandons pour déchiffrer des emails chiffrés en Python.
331+
332+
A noter néanmoins que l'intégration de cette nouvelle fonctionnalité à `cryptography` a pris du
333+
temps à l'équipe SCIAM, entre développement et revues de code. Une contribution open-source prend du
334+
temps, mais c'est un investissement qui en vaut la peine !
335+
336+
== Le Notebook
337+
338+
Vous pouvez retrouver le notebook Jupyter complet de cet article sur notre dépôt GitHub :
339+
https://github.com/SCIAM-FR/email-decryption-demo[SCIAM-FR/email-decryption-demo].
340+
341+
== Les liens utiles
342+
* https://cryptography.io/en/latest/[Cryptography Documentation]
343+
* https://github.com/wbond/asn1crypto[ASN.1 Crypto Documentation]
344+
* 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)