Skip to content

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
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .vscode/settings.json
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
}
11 changes: 11 additions & 0 deletions _data/authors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ christophesejourne:
socials:
linkedin: "christophesejourne"


quentinretourne:
name: "Quentin Retourné"
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 !"

job: "Consultant Sénior Data/IA"
pagesciam: "https://www.sciam.fr/equipe/quentin-retourne"
picture: quentinretourne.jpg
socials:
linkedin: "quentinretourne"

pierrelepagnol:
name: "Pierre Lepagnol"
bio: "PhD Student in CS @LISN/Paris-Saclay University & Data Scientist"
Expand Down
362 changes: 362 additions & 0 deletions _posts/2024-12-15-dechiffrement-email-python.adoc
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
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
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]
----
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]
----
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
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]
Binary file added images/authors/quentinretourne.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/vignettes/email-decryption.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading