-
Notifications
You must be signed in to change notification settings - Fork 4
Article/email decryption #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nitneuqr
wants to merge
1
commit into
main
Choose a base branch
from
article/email-decryption
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
{ | ||
"asciidoc.extensions.enableKroki": true, | ||
"asciidoc.preview.asciidoctorAttributes": { | ||
"imagesdir":"../images", | ||
"source-highlighter":"highlightjs", | ||
"stem":"latexmath" | ||
"imagesdir": "../images", | ||
"source-highlighter": "highlightjs", | ||
"stem": "latexmath" | ||
}, | ||
"cSpell.language": "en,fr,fr-FR", | ||
"asciidoc.antora.enableAntoraSupport": false | ||
"asciidoc.antora.showEnableAntoraPrompt": true | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,362 @@ | ||
= Déchiffrement d'Emails avec Python | ||
:showtitle: | ||
:page-navtitle: Déchiffrement d'Emails avec Python | ||
:page-excerpt: Découvrez comment déchiffrer des emails chiffrés avec Python en utilisant `openssl`, `asn1crypto` et `cryptography`. | ||
:layout: post | ||
:author: quentinretourne | ||
:page-tags: [Tutoriel, Python, Cryptographie] | ||
:page-vignette: email-decryption.png | ||
:page-categories: software | ||
|
||
Le déchiffrement d'emails chiffrés est une tâche essentielle pour garantir la confidentialité des | ||
communications. Dans cet article, nous présentons différentes solutions pour déchiffrer des emails | ||
chiffrés en utilisant Python, en particulier avec l'exécutable `openssl` et les bibliothèques | ||
`asn1crypto` et `cryptography`. Nous terminons en présentant une nouvelle méthode de déchiffrement | ||
de mails chiffrés, développée par nos soins, qui vient d'être intégrée à `cryptography` en novembre | ||
2024. | ||
|
||
== Quelques prérequis | ||
|
||
Avant de se plonger dans le déchiffrement des emails, il est important de comprendre quelques concepts | ||
de base en cryptographie. | ||
|
||
=== La cryptographie et le chiffrement | ||
|
||
La cryptographie est l'art de sécuriser les communications en les transformant afin qu'elles ne | ||
puissent être lues que par les destinataires prévus. Un message chiffré n'aura pas de sens pour un | ||
observateur externe. En cryptographie, il s'agit donc de chiffrer et de déchiffrer des messages. | ||
Pour ce faire, plusieurs algorithmes de chiffrement sont utilisés ; nous allons parcourir ceux qui | ||
nous seront utiles dans cet article. | ||
|
||
Le https://fr.wikipedia.org/wiki/Chiffrement_RSA[chiffrement RSA] est un algorithme de cryptographie | ||
asymétrique qui utilise une paire de clés : une clé publique pour le chiffrement et une clé privée | ||
pour le déchiffrement. RSA est largement utilisé pour sécuriser les communications sur Internet, | ||
notamment pour les certificats SSL/TLS et les connexions SSH. | ||
|
||
Par opposition au chiffrement asymétrique (dont RSA fait partie), on peut aussi chiffrer des | ||
messages de manière symétrique. Dans ce cas, on utilise la même clé pour chiffrer et déchiffrer les | ||
messages. Cela est utile pour d'autres cas d'usages : le chiffrement symétrique est généralement | ||
plus rapide que le chiffrement asymétrique (même si cela dépend des algorithmes utilisés). | ||
|
||
Ces deux types de chiffrement sont utilisés dans le format PKCS #7 pour chiffrer des emails, que | ||
nous allons étudier ci-dessous. | ||
|
||
=== Le format PKCS #7 | ||
|
||
Les spécifications https://en.wikipedia.org/wiki/PKCS_7[PKCS #7] (Public Key Cryptography Standards | ||
#7) définissent un format standard pour sécuriser des messages, et notamment des emails. PKCS #7 est | ||
un format complet, qui permet notamment de signer / vérifier des messages ainsi que les chiffrer / | ||
déchiffrer. Dans cet article, nous allons nous concentrer sur les spécifications de "chiffrement / | ||
déchiffrement" de PKCS #7 par opposition à la partie "signature". On les désigne aussi par les | ||
termes "encapsulation" ou "enveloped data" en anglais. | ||
|
||
Dans le cas de chiffrement d'un message PKCS #7, deux chiffrements ont lieu: | ||
|
||
1. Le chiffrement du contenu du message, selon un algorithme de chiffrement symétrique nécessitant | ||
nitneuqr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
une clé. Cette clé est générée de manière aléatoire. | ||
2. Le(s) chiffrement(s) de la clé ci-dessus, cette fois-ci de façon asymétrique, pour chaque destinataire | ||
du message. Cela est fait avec la/les clé(s) RSA publique(s) du ou des destinataires, qui doivent | ||
être mises à disposition de l'expéditeur du message. | ||
|
||
Ainsi, lors du déchiffrement, chaque destinataire va : | ||
|
||
- Inspecter chaque clé chiffrée dans le message sous format PKCS #7, et trouver celle qui | ||
nitneuqr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
correspond à sa clé RSA publique. | ||
- Déchiffrer la clé symétrique avec sa clé RSA privée. | ||
- Déchiffrer le contenu du message avec la clé symétrique fraîchement déchiffrée. | ||
|
||
L'avantage de cette méthode permet d'économiser du temps de calcul et de réduire la taille du | ||
message à transporter. En effet, le contenu (qui n'a pas de limite de taille) est chiffré une seule | ||
fois avec un algorithme symétrique, beaucoup plus rapide que ses homologues asymétriques. De plus, | ||
c'est uniquement la clé symétrique (entre 128 et 256 bits selon les algorithmes) qui est chiffrée | ||
plusieurs fois pour chaque destinataire par un algorithme asymétrique et stockée dans la structure | ||
du message. | ||
|
||
== L'analyse d'un message chiffré | ||
|
||
La structure https://fr.wikipedia.org/wiki/Abstract_Syntax_Notation_One[ASN.1] (Abstract Syntax | ||
Notation One) est un standard de notation utilisé pour représenter des données, couramment utilisé | ||
dans les protocoles de communication et les certificats numériques. | ||
|
||
Nous allons étudier un message chiffré, stocké dans le fichier `enveloped.der`. Les messages PKCS#7 | ||
sont toujours encodés dans une structure ASN.1. Ici, elle est au format DER, un format binaire | ||
classique pour ce type de structure. | ||
|
||
=== Avec OpenSSL | ||
|
||
OpenSSL est une bibliothèque logicielle open-source qui fournit des implémentations de nombreux | ||
protocoles de sécurité. Elle est fournie notamment sous forme d'un exécutable en ligne de commande | ||
qui permet de manipuler des certificats, des clés et des messages chiffrés. Cet exécutable est | ||
généralement installé par défaut dans les environnements Conda pour Python, ce qui est très | ||
pratique. Ainsi, on peut l'utiliser tel quel via la bibliothèque built-in `subprocess` : | ||
|
||
[source, python] | ||
nitneuqr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
---- | ||
import subprocess | ||
|
||
instructions = [ | ||
"openssl", | ||
"asn1parse", <1> | ||
"-in", | ||
"vectors/enveloped.der", | ||
"-inform", | ||
"der", | ||
] | ||
output = subprocess.run(instructions, check=True, capture_output=True) <2> | ||
print(output.stdout.decode()) | ||
---- | ||
<1> La commande `asn1parse` d' `openssl` permet d'analyser la structure ASN.1 du message chiffré. | ||
<2> On utilise `check=True` pour lever une exception si la commande `openssl` échoue. | ||
|
||
Résultat : | ||
[source, cmd] | ||
---- | ||
0:d=0 hl=4 l= 667 cons: SEQUENCE | ||
4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-envelopedData <1> | ||
15:d=1 hl=4 l= 652 cons: cont [ 0 ] | ||
19:d=2 hl=4 l= 648 cons: SEQUENCE | ||
23:d=3 hl=2 l= 1 prim: INTEGER :00 | ||
26:d=3 hl=4 l= 579 cons: SET | ||
30:d=4 hl=4 l= 575 cons: SEQUENCE | ||
34:d=5 hl=2 l= 1 prim: INTEGER :00 | ||
37:d=5 hl=2 l= 39 cons: SEQUENCE | ||
39:d=6 hl=2 l= 26 cons: SEQUENCE | ||
41:d=7 hl=2 l= 24 cons: SET | ||
43:d=8 hl=2 l= 22 cons: SEQUENCE | ||
45:d=9 hl=2 l= 3 prim: OBJECT :commonName <2> | ||
50:d=9 hl=2 l= 15 prim: UTF8STRING :cryptography CA | ||
67:d=6 hl=2 l= 9 prim: INTEGER :E712D3A0A56ED6C9 | ||
78:d=5 hl=2 l= 13 cons: SEQUENCE | ||
80:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption <3> | ||
91:d=6 hl=2 l= 0 prim: NULL | ||
93:d=5 hl=4 l= 512 prim: OCTET STRING [HEX DUMP]:08086584F1DD436CDA1FB527B243FA02 | ||
609:d=3 hl=2 l= 60 cons: SEQUENCE | ||
611:d=4 hl=2 l= 9 prim: OBJECT :pkcs7-data | ||
622:d=4 hl=2 l= 29 cons: SEQUENCE | ||
624:d=5 hl=2 l= 9 prim: OBJECT :aes-128-cbc <4> | ||
635:d=5 hl=2 l= 16 prim: OCTET STRING [HEX DUMP]:2CD7875912507DFC7E65EA7CB86C73BB | ||
653:d=4 hl=2 l= 16 prim: cont [ 0 ] | ||
---- | ||
<1> Le type de message est `pkcs7-envelopedData`. | ||
<2> On voit bien un seul certificat, avec un numéro de série `E712D3A0A56ED6C9`. | ||
<3> L'algorithme de chiffrement est `rsaEncryption`, avec la clé chiffrée ci-dessous. | ||
<4> Le contenu est chiffré avec l'algorithme AES, avec une clé de 128 | ||
bits et le mode CBC. Son contenu est ci-dessous. | ||
|
||
=== Avec `asn1crypto` | ||
|
||
On peut faire le même exercice en utilisant la librairie `asn1crypto`. Cette bibliothèque permet de | ||
manipuler des structures ASN.1 de manière plus aisée que `openssl`, en offrant des classes Python | ||
qui peuvent être sérialisées et désérialisées facilement en dictionnaires. Dans notre cas, prenons | ||
l'exemple de la lecture d'une partie de la structure du message chiffré : | ||
|
||
[source, python] | ||
---- | ||
from asn1crypto import cms | ||
|
||
with open("vectors/enveloped.der", "rb") as file: | ||
enveloped = file.read() | ||
|
||
# Charger la structure ASN.1 | ||
content_info = cms.ContentInfo.load(enveloped) <1> | ||
content_type: cms.ContentType = content_info["content_type"] | ||
enveloped_data: cms.EnvelopedData = content_info["content"] | ||
|
||
# Le champ "Encrypted Content Info" contient le contenu chiffré | ||
encrypted_content_info: cms.EncryptedContentInfo = enveloped_data["encrypted_content_info"] | ||
dict(encrypted_content_info.native) | ||
---- | ||
<1> Il faut connaître en amont la structure ASN.1 du message pour pouvoir la charger et utiliser le | ||
typage correctement. Cela demande une connaissance préalable des spécifications PKCS #7. | ||
|
||
Résultat : | ||
|
||
[source, python] | ||
---- | ||
{ | ||
"content_encryption_algorithm": { <1> | ||
"algorithm": "aes128_cbc", | ||
"parameters": b",\xd7\x87Y\x12P}\xfc~e\xea|\xb8ls\xbb", | ||
}, | ||
"content_type": "data", | ||
"encrypted_content": b"[tN\xcb\xdd]\x0b\xa2\xa2\x98T\xf8[t_`", | ||
} | ||
---- | ||
<1> On retrouve la même structure que lors de l'analyse avec `openssl`. | ||
|
||
== Le déchiffrement | ||
|
||
Nous allons maintenant déchiffrer le message chiffré. Pour cela, nous avons besoin des différentes | ||
informations du destinataire, notamment son certificat X.509 et sa clé privée RSA. Ensuite, nous | ||
verrons 3 méthodes pour déchiffrer le message : avec `openssl`, avec `asn1crypto` et avec | ||
`cryptography`. | ||
|
||
=== La lecture du certificat et de la clé privée | ||
|
||
Nous lisons le certificat X.509 et la clé privée RSA, avec la librairie `cryptography` : | ||
|
||
[source, python] | ||
---- | ||
from cryptography.hazmat.primitives.serialization import load_pem_private_key | ||
from cryptography.x509 import load_pem_x509_certificate | ||
|
||
# Clé publique : certificat RSA | ||
with open("vectors/rsa_ca.pem", "rb") as file: | ||
certificate = load_pem_x509_certificate(file.read()) | ||
|
||
# Clé privée : RSA | ||
with open("vectors/rsa_key.pem", "rb") as file: | ||
private_key = load_pem_private_key(file.read(), password=None) | ||
---- | ||
|
||
=== Avec OpenSSL | ||
Nous utilisons `openssl` pour déchiffrer le message : | ||
|
||
[source, python] | ||
---- | ||
import subprocess | ||
|
||
instructions = [ | ||
"openssl", | ||
"smime", | ||
"-decrypt", <1> | ||
"-in", | ||
"vectors/enveloped.der", | ||
"-inkey", | ||
"vectors/rsa_key.pem", <2> | ||
"-inform", | ||
"der", | ||
] | ||
output = subprocess.run(instructions, capture_output=True) | ||
output.stdout.decode() | ||
---- | ||
<1> La commande `smime -decrypt` permet de déchiffrer un message PKCS #7. Il en existe d'autres | ||
comme `smime -encrypt` ou `cms -decrypt` pour des spécifications PKCS # 7 plus récentes. | ||
<2> Dans cette commande, on ne précise pas le certificat X.509. Il semble qu' `openssl` tente de | ||
déchiffrer le message avec la clé privée fournie, sans vérifier si elle correspond au certificat | ||
dans le message chiffré. | ||
|
||
=== Avec `asn1crypto` | ||
Nous utilisons `asn1crypto` pour analyser et déchiffrer le message : | ||
|
||
[source, python] | ||
nitneuqr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
---- | ||
from asn1crypto import cms | ||
from cryptography.hazmat.primitives import padding | ||
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding | ||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | ||
|
||
# Charger le message chiffré | ||
with open("vectors/enveloped.der", "rb") as file: | ||
enveloped = file.read() | ||
|
||
# Charger la structure ASN.1 | ||
content_info = cms.ContentInfo.load(enveloped) | ||
content_type = content_info["content_type"] | ||
enveloped_data = content_info["content"] | ||
|
||
# Déchiffrement de la clés de chiffrement symétrique, si possible | ||
decrypted_key = None | ||
for recipient_info in enveloped_data["recipient_infos"].native: | ||
if recipient_info["rid"]["serial_number"] == certificate.serial_number: | ||
decrypted_key = private_key.decrypt( | ||
recipient_info["encrypted_key"], asymmetric_padding.PKCS1v15() <1> | ||
) | ||
break | ||
|
||
# On lève une erreur si aucune clé n'est déchiffrable | ||
if decrypted_key is None: | ||
raise ValueError("Aucune clé chiffrée n'est déchiffrable pour le destinataire donné.") | ||
|
||
# Déchiffrement du contenu en utilisant la clé symétrique fraîchement déchiffrée | ||
def decrypt(ciphertext, key, initialization_vector) -> str: | ||
cipher = Cipher(algorithms.AES(key), modes.CBC(initialization_vector)) <2> | ||
decryptor = cipher.decryptor() | ||
padded_data = decryptor.update(ciphertext) + decryptor.finalize() | ||
unpadder = padding.PKCS7(len(key) * 8).unpadder() <3> | ||
plaintext = unpadder.update(padded_data) + unpadder.finalize() | ||
return plaintext.decode() | ||
|
||
encrypted = enveloped_data["encrypted_content_info"].native | ||
decrypted_content = decrypt( | ||
encrypted["encrypted_content"], | ||
decrypted_key, | ||
encrypted["content_encryption_algorithm"]["parameters"], | ||
) | ||
print("Decrypted content:", decrypted_content) | ||
---- | ||
<1> On suppose ici que le chiffrement de la clé symétrique a été fait avec l'algorithme | ||
`rsaEncryption` et le padding `PKCS1v15`. Cela peut varier selon les implémentations : une autre | ||
possibilité est `RSAES-OAEP` pour un padding plus sécurisé. | ||
<2> On suppose ici que l'algorithme de chiffrement du contenu est AES au mode CBC. AES est | ||
l'algorithme le plus répandu, mais d'autres modes que CBC, comme GCM, sont utilisés dans des cas | ||
plus récents. | ||
<3> On utilise la taille de la clé (multipliée par 8, pour passer des octects aux bits) pour | ||
déterminer la taille du padding. Deux tailles sont classiques : 128 et 256 bits. | ||
|
||
|
||
=== Avec `cryptography` | ||
|
||
Enfin, nous utilisons la nouvelle méthode de déchiffrement de messages PKCS #7 de la version 44.0.0 | ||
de `cryptography`, sortie le 27 novembre 2024 : | ||
|
||
[source, python] | ||
---- | ||
from cryptography.hazmat.primitives.serialization import pkcs7 | ||
|
||
with open("vectors/enveloped.der", "rb") as file: | ||
enveloped = file.read() | ||
|
||
decrypted = pkcs7.decrypt_der(enveloped, certificate, private_key, []) <1> | ||
print("Decrypted content:", decrypted_content) | ||
---- | ||
<1> Le dernier argument est une liste d'options facultatives (voir ci-dessous). Ici, on n'en passe | ||
aucune. | ||
|
||
Cette fonctionnalité simple d'utilisation se décline selon les formats des fichiers d'entrée : | ||
|
||
- `decrypt_der` pour for format `DER`. | ||
- `decrypt_pem` pour le format `PEM`. | ||
- `decrypt_smime` pour email sous format texte contenant le message PKCS #7 en pièce jointe. | ||
|
||
Elle s'occupe automatiquement de comparer les numéros de série des certificats dans le message | ||
chiffré avec ceux du certificat fourni, gère les deux phases de déchiffrement, et propose des | ||
messages d'erreur compréhensibles en cas de problème (clé privée manquante, algorithme non supporté, | ||
etc.). | ||
|
||
Elle possède aussi des options pour nettoyer le texte déchiffré, afin de se rapprocher au maximum du | ||
fonctionnement d' `openssl`. Cela permet un remplacement facile de l'utilisation d' `openssl` dans | ||
les scripts Python. | ||
|
||
== Conclusion | ||
Nous avons présenté différentes méthodes pour déchiffrer des emails chiffrés en utilisant Python. | ||
Chacune de ces méthodes ont leurs avantages et leurs inconvénients. | ||
|
||
`openssl` est un exécutable facile d'accès contenant toutes les fonctionnalités cryptographiques à | ||
portée de la ligne de commande. Cependant, pour des raisons de sécurité, il est déconseillé | ||
d'utiliser OpenSSL directement, car il a déjà exposé des vulnérabilités par le passé. De plus, c'est | ||
une dépendance supplémentaire, bien qu'elle soit facile à régler via Conda. | ||
|
||
La méthode utilisant `asn1crypto` permet de réaliser le déchiffrement 100% en Python, sans | ||
dépendance particulière. Néanmoins, elle demande des connaissances plus avancées en cryptographie et | ||
ne constitue pas une solution clé en main. En effet, la gestion d'autres cas d'usages de PKCS #7 | ||
(signature, autres algorithmes) demande de la réflexion. De plus, `asn1crypto` est n'est plus | ||
maintenue depuis fin 2022. | ||
|
||
Enfin, la méthode utilisant `cryptography` est la plus simple et la plus sécurisée. Elle est | ||
installée dans une librairie maintenue, et est fiable grâce à l'utilisation de Python & Rust. C'est | ||
la méthode que nous recommandons pour déchiffrer des emails chiffrés en Python. | ||
|
||
A noter néanmoins que l'intégration de cette nouvelle fonctionnalité à `cryptography` a pris du | ||
nitneuqr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
temps à l'équipe SCIAM, entre développement et revues de code. Une contribution open-source prend du | ||
temps, mais c'est un investissement qui en vaut la peine ! | ||
|
||
== Le notebook | ||
|
||
Vous pouvez retrouver le notebook Jupyter complet de cet article sur notre dépôt GitHub : | ||
https://github.com/SCIAM-FR/email-decryption-demo[SCIAM-FR/email-decryption-demo]. | ||
|
||
== Les liens utiles | ||
* https://cryptography.io/en/latest/[Cryptography Documentation] | ||
* https://github.com/wbond/asn1crypto[ASN.1 Crypto Documentation] | ||
* https://www.baeldung.com/cs/public-key-cryptography-standards[Article sur PKCS #7] |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.